From 04ed731a59aa109df45f27176de972ecd077e081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 9 Oct 2025 15:04:55 +0200 Subject: [PATCH 01/26] improve: add license headers to source files (#2980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/processing/event/source/CacheKeyMapper.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java new file mode 100644 index 0000000000..e69de29bb2 From 749e1e09b459e2b6940ea15c8630fdcbbb92e1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 14 Oct 2025 15:54:26 +0200 Subject: [PATCH 02/26] chore: version to 5.3.0-SNAPSHOT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- bootstrapper-maven-plugin/pom.xml | 2 +- caffeine-bounded-cache-support/pom.xml | 2 +- micrometer-support/pom.xml | 2 +- operator-framework-bom/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/controller-namespace-deletion/pom.xml | 2 +- sample-operators/leader-election/pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml index b3271c1c4f..364495fca9 100644 --- a/bootstrapper-maven-plugin/pom.xml +++ b/bootstrapper-maven-plugin/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT bootstrapper diff --git a/caffeine-bounded-cache-support/pom.xml b/caffeine-bounded-cache-support/pom.xml index 8d3546a718..a050e0ff3c 100644 --- a/caffeine-bounded-cache-support/pom.xml +++ b/caffeine-bounded-cache-support/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT caffeine-bounded-cache-support diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index f1e174c856..93765e4a8a 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT micrometer-support diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index 2f3bef6fdf..1ffa1701f8 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk operator-framework-bom - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT pom Operator SDK - Bill of Materials Java SDK for implementing Kubernetes operators diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index bb1baad08b..361bb3ad53 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index e3072dd17a..60c235a9ec 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT operator-framework-junit-5 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 7b4a12824d..253907eb1e 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT operator-framework diff --git a/pom.xml b/pom.xml index 415aa79cbc..67e0f45302 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT pom Operator SDK for Java Java SDK for implementing Kubernetes operators diff --git a/sample-operators/controller-namespace-deletion/pom.xml b/sample-operators/controller-namespace-deletion/pom.xml index b8994fe20b..ddfeebacbc 100644 --- a/sample-operators/controller-namespace-deletion/pom.xml +++ b/sample-operators/controller-namespace-deletion/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-controller-namespace-deletion diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index 316b926fef..70485a2f3e 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-leader-election diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 86eb91bee0..a2334ca8c6 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 820a110afb..6079d3bb71 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index f07760ec28..c9fe8c2d06 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 03d363212c..e25920b7da 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-webpage-operator From 6716a10207acf3079c72c04f1a5c2a2ac564cba9 Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Thu, 30 Oct 2025 08:22:09 -0400 Subject: [PATCH 03/26] Annotation removal using locking (#3015) Signed-off-by: Steve Hawkins --- .../api/config/ConfigurationService.java | 63 +---- .../config/ConfigurationServiceOverrider.java | 45 +--- .../InformerEventSourceConfiguration.java | 22 +- .../operator/api/reconciler/Constants.java | 1 + .../PrimaryUpdateAndCacheUtils.java | 41 +++ .../KubernetesDependentResource.java | 46 ++-- .../controller/ControllerEventSource.java | 6 +- .../source/informer/InformerEventSource.java | 133 +++------- .../source/informer/InformerWrapper.java | 4 + .../informer/ManagedInformerEventSource.java | 39 +-- .../informer/TemporaryResourceCache.java | 237 +++++++++--------- .../informer/InformerEventSourceTest.java | 19 +- .../TemporaryPrimaryResourceCacheTest.java | 139 +++++----- ...ComparableResourceVersionsDisabledIT.java} | 0 .../ExternalStateReconciler.java | 7 +- 15 files changed, 361 insertions(+), 441 deletions(-) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/{PreviousAnnotationDisabledIT.java => ComparableResourceVersionsDisabledIT.java} (100%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 6215c20179..99bb280ae4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -28,8 +28,6 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.CustomResource; @@ -46,6 +44,8 @@ import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSIONS; + /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { @@ -448,61 +448,16 @@ default Set> defaultNonSSAResource() { } /** - * If a javaoperatorsdk.io/previous annotation should be used so that the operator sdk can detect - * events from its own updates of dependent resources and then filter them. + * If the event logic can compare resourceVersions. * - *

Disable this if you want to react to your own dependent resource updates + *

Enabled by default as Kubernetes does support this interpretation of resourceVersions. + * Disable only if your api server provides non comparable resource versions. * - * @return if special annotation should be used for dependent resource to filter events - * @since 4.5.0 + * @return if resource versions are comparable + * @since 5.3.0 */ - default boolean previousAnnotationForDependentResourcesEventFiltering() { - return true; - } - - /** - * For dependent resources, the framework can add an annotation to filter out events resulting - * directly from the framework's operation. There are, however, some resources that do not follow - * the Kubernetes API conventions that changes in metadata should not increase the generation of - * the resource (as recorded in the {@code generation} field of the resource's {@code metadata}). - * For these resources, this convention is not respected and results in a new event for the - * framework to process. If that particular case is not handled correctly in the resource matcher, - * the framework will consider that the resource doesn't match the desired state and therefore - * triggers an update, which in turn, will re-add the annotation, thus starting the loop again, - * infinitely. - * - *

As a workaround, we automatically skip adding previous annotation for those well-known - * resources. Note that if you are sure that the matcher works for your use case, and it should in - * most instances, you can remove the resource type from the blocklist. - * - *

The consequence of adding a resource type to the set is that the framework will not use - * event filtering to prevent events, initiated by changes made by the framework itself as a - * result of its processing of dependent resources, to trigger the associated reconciler again. - * - *

Note that this method only takes effect if annotating dependent resources to prevent - * dependent resources events from triggering the associated reconciler again is activated as - * controlled by {@link #previousAnnotationForDependentResourcesEventFiltering()} - * - * @return a Set of resource classes where the previous version annotation won't be used. - */ - default Set> withPreviousAnnotationForDependentResourcesBlocklist() { - return Set.of(Deployment.class, StatefulSet.class); - } - - /** - * If the event logic should parse the resourceVersion to determine the ordering of dependent - * resource events. This is typically not needed. - * - *

Disabled by default as Kubernetes does not support, and discourages, this interpretation of - * resourceVersions. Enable only if your api server event processing seems to lag the operator - * logic, and you want to further minimize the amount of work done / updates issued by the - * operator. - * - * @return if resource version should be parsed (as integer) - * @since 4.5.0 - */ - default boolean parseResourceVersionsForEventFilteringAndCaching() { - return false; + default boolean comparableResourceVersions() { + return DEFAULT_COMPARABLE_RESOURCE_VERSIONS; } /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index 3d29bb6589..81a5428044 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -51,11 +51,9 @@ public class ConfigurationServiceOverrider { private Duration reconciliationTerminationTimeout; private Boolean ssaBasedCreateUpdateMatchForDependentResources; private Set> defaultNonSSAResource; - private Boolean previousAnnotationForDependentResources; - private Boolean parseResourceVersions; + private Boolean comparableResourceVersions; private Boolean useSSAToPatchPrimaryResource; private Boolean cloneSecondaryResourcesWhenGettingFromCache; - private Set> previousAnnotationUsageBlocklist; @SuppressWarnings("rawtypes") private DependentResourceFactory dependentResourceFactory; @@ -168,28 +166,23 @@ public ConfigurationServiceOverrider withDefaultNonSSAResource( return this; } - public ConfigurationServiceOverrider withPreviousAnnotationForDependentResources(boolean value) { - this.previousAnnotationForDependentResources = value; - return this; - } - /** * @param value true if internal algorithms can use metadata.resourceVersion as a numeric value. * @return this */ - public ConfigurationServiceOverrider withParseResourceVersions(boolean value) { - this.parseResourceVersions = value; + public ConfigurationServiceOverrider withComparableResourceVersions(boolean value) { + this.comparableResourceVersions = value; return this; } /** - * @deprecated use withParseResourceVersions + * @deprecated use withComparableResourceVersions * @param value true if internal algorithms can use metadata.resourceVersion as a numeric value. * @return this */ @Deprecated(forRemoval = true) - public ConfigurationServiceOverrider wihtParseResourceVersions(boolean value) { - this.parseResourceVersions = value; + public ConfigurationServiceOverrider withParseResourceVersions(boolean value) { + this.comparableResourceVersions = value; return this; } @@ -204,12 +197,6 @@ public ConfigurationServiceOverrider withCloneSecondaryResourcesWhenGettingFromC return this; } - public ConfigurationServiceOverrider withPreviousAnnotationForDependentResourcesBlocklist( - Set> blocklist) { - this.previousAnnotationUsageBlocklist = blocklist; - return this; - } - public ConfigurationService build() { return new BaseConfigurationService(original.getVersion(), cloner, client) { @Override @@ -331,20 +318,6 @@ public Set> defaultNonSSAResources() { defaultNonSSAResource, ConfigurationService::defaultNonSSAResources); } - @Override - public boolean previousAnnotationForDependentResourcesEventFiltering() { - return overriddenValueOrDefault( - previousAnnotationForDependentResources, - ConfigurationService::previousAnnotationForDependentResourcesEventFiltering); - } - - @Override - public boolean parseResourceVersionsForEventFilteringAndCaching() { - return overriddenValueOrDefault( - parseResourceVersions, - ConfigurationService::parseResourceVersionsForEventFilteringAndCaching); - } - @Override public boolean useSSAToPatchPrimaryResource() { return overriddenValueOrDefault( @@ -359,11 +332,9 @@ public boolean cloneSecondaryResourcesWhenGettingFromCache() { } @Override - public Set> - withPreviousAnnotationForDependentResourcesBlocklist() { + public boolean comparableResourceVersions() { return overriddenValueOrDefault( - previousAnnotationUsageBlocklist, - ConfigurationService::withPreviousAnnotationForDependentResourcesBlocklist); + comparableResourceVersions, ConfigurationService::comparableResourceVersions); } }; } 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 bca605a41c..c6ea21f0c0 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 @@ -33,6 +33,7 @@ import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSIONS; import static io.javaoperatorsdk.operator.api.reconciler.Constants.SAME_AS_CONTROLLER_NAMESPACES_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_ALL_NAMESPACE_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE_SET; @@ -96,18 +97,21 @@ class DefaultInformerEventSourceConfiguration private final GroupVersionKind groupVersionKind; private final InformerConfiguration informerConfig; private final KubernetesClient kubernetesClient; + private final boolean comparableResourceVersions; protected DefaultInformerEventSourceConfiguration( GroupVersionKind groupVersionKind, PrimaryToSecondaryMapper primaryToSecondaryMapper, SecondaryToPrimaryMapper secondaryToPrimaryMapper, InformerConfiguration informerConfig, - KubernetesClient kubernetesClient) { + KubernetesClient kubernetesClient, + boolean comparableResourceVersions) { this.informerConfig = Objects.requireNonNull(informerConfig); this.groupVersionKind = groupVersionKind; this.primaryToSecondaryMapper = primaryToSecondaryMapper; this.secondaryToPrimaryMapper = secondaryToPrimaryMapper; this.kubernetesClient = kubernetesClient; + this.comparableResourceVersions = comparableResourceVersions; } @Override @@ -135,6 +139,11 @@ public Optional getGroupVersionKind() { public Optional getKubernetesClient() { return Optional.ofNullable(kubernetesClient); } + + @Override + public boolean comparableResourceVersions() { + return this.comparableResourceVersions; + } } @SuppressWarnings({"unused", "UnusedReturnValue"}) @@ -148,6 +157,7 @@ class Builder { private PrimaryToSecondaryMapper primaryToSecondaryMapper; private SecondaryToPrimaryMapper secondaryToPrimaryMapper; private KubernetesClient kubernetesClient; + private boolean comparableResourceVersions = DEFAULT_COMPARABLE_RESOURCE_VERSIONS; private Builder(Class resourceClass, Class primaryResourceClass) { this(resourceClass, primaryResourceClass, null); @@ -285,6 +295,11 @@ public Builder withFieldSelector(FieldSelector fieldSelector) { return this; } + public Builder withComparableResourceVersions(boolean comparableResourceVersions) { + this.comparableResourceVersions = comparableResourceVersions; + return this; + } + public void updateFrom(InformerConfiguration informerConfig) { if (informerConfig != null) { final var informerConfigName = informerConfig.getName(); @@ -324,7 +339,10 @@ public InformerEventSourceConfiguration build() { HasMetadata.getKind(primaryResourceClass), false)), config.build(), - kubernetesClient); + kubernetesClient, + comparableResourceVersions); } } + + boolean comparableResourceVersions(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java index 052b4d8c44..ed975d71ef 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java @@ -41,6 +41,7 @@ public final class Constants { public static final String RESOURCE_GVK_KEY = "josdk.resource.gvk"; public static final String CONTROLLER_NAME = "controller.name"; public static final boolean DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES = true; + public static final boolean DEFAULT_COMPARABLE_RESOURCE_VERSIONS = true; private Constants() {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java index 6103b4b12b..11dfd21648 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java @@ -450,4 +450,45 @@ public static

P addFinalizerWithSSA( e); } } + + public static int compareResourceVersions(HasMetadata h1, HasMetadata h2) { + return compareResourceVersions( + h1.getMetadata().getResourceVersion(), h2.getMetadata().getResourceVersion()); + } + + public static int compareResourceVersions(String v1, String v2) { + int v1Length = validateResourceVersion(v1); + int v2Length = validateResourceVersion(v2); + int comparison = v1Length - v2Length; + if (comparison != 0) { + return comparison; + } + for (int i = 0; i < v2Length; i++) { + int comp = v1.charAt(i) - v2.charAt(i); + if (comp != 0) { + return comp; + } + } + return 0; + } + + private static int validateResourceVersion(String v1) { + int v1Length = v1.length(); + if (v1Length == 0) { + throw new NonComparableResourceVersionException("Resource version is empty"); + } + for (int i = 0; i < v1Length; i++) { + char char1 = v1.charAt(i); + if (char1 == '0') { + if (i == 0) { + throw new NonComparableResourceVersionException( + "Resource version cannot begin with 0: " + v1); + } + } else if (char1 < '0' || char1 > '9') { + throw new NonComparableResourceVersionException( + "Non numeric characters in resource version: " + v1); + } + } + return v1Length; + } } 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 05cddcade1..562a6257b5 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 @@ -55,7 +55,6 @@ public abstract class KubernetesDependentResource kubernetesDependentResourceConfig; private volatile Boolean useSSA; - private volatile Boolean usePreviousAnnotationForEventFiltering; public KubernetesDependentResource() {} @@ -72,6 +71,27 @@ public void configureWith(KubernetesDependentResourceConfig config) { this.kubernetesDependentResourceConfig = config; } + @Override + protected R handleCreate(R desired, P primary, Context

context) { + return eventSource() + .orElseThrow() + .updateAndCacheResource( + desired, + context, + toCreate -> KubernetesDependentResource.super.handleCreate(toCreate, primary, context)); + } + + @Override + protected R handleUpdate(R actual, R desired, P primary, Context

context) { + return eventSource() + .orElseThrow() + .updateAndCacheResource( + desired, + context, + toUpdate -> + KubernetesDependentResource.super.handleUpdate(actual, toUpdate, primary, context)); + } + @SuppressWarnings("unused") public R create(R desired, P primary, Context

context) { if (useSSA(context)) { @@ -158,14 +178,6 @@ protected void addMetadata( } else { annotations.remove(InformerEventSource.PREVIOUS_ANNOTATION_KEY); } - } else if (usePreviousAnnotation(context)) { // set a new one - eventSource() - .orElseThrow() - .addPreviousAnnotation( - Optional.ofNullable(actualResource) - .map(r -> r.getMetadata().getResourceVersion()) - .orElse(null), - target); } addReferenceHandlingMetadata(target, primary); } @@ -181,22 +193,6 @@ protected boolean useSSA(Context

context) { return useSSA; } - private boolean usePreviousAnnotation(Context

context) { - if (usePreviousAnnotationForEventFiltering == null) { - usePreviousAnnotationForEventFiltering = - context - .getControllerConfiguration() - .getConfigurationService() - .previousAnnotationForDependentResourcesEventFiltering() - && !context - .getControllerConfiguration() - .getConfigurationService() - .withPreviousAnnotationForDependentResourcesBlocklist() - .contains(this.resourceType()); - } - return usePreviousAnnotationForEventFiltering; - } - @Override protected void handleDelete(P primary, R secondary, Context

context) { if (secondary != null) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java index b7a6406e20..59d86efe48 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java @@ -47,7 +47,11 @@ public class ControllerEventSource @SuppressWarnings({"unchecked", "rawtypes"}) public ControllerEventSource(Controller controller) { - super(NAME, controller.getCRClient(), controller.getConfiguration(), false); + super( + NAME, + controller.getCRClient(), + controller.getConfiguration(), + controller.getConfiguration().getConfigurationService().comparableResourceVersions()); this.controller = controller; final var config = controller.getConfiguration(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index ec11db25f4..d46dd0669a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -17,7 +17,7 @@ import java.util.Optional; import java.util.Set; -import java.util.UUID; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -35,6 +35,8 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSIONS; + /** * Wraps informer(s) so they are connected to the eventing system of the framework. Note that since * this is built on top of Fabric8 client Informers, it also supports caching resources using @@ -78,28 +80,24 @@ public class InformerEventSource // we need direct control for the indexer to propagate the just update resource also to the index private final PrimaryToSecondaryIndex primaryToSecondaryIndex; private final PrimaryToSecondaryMapper

primaryToSecondaryMapper; - private final String id = UUID.randomUUID().toString(); public InformerEventSource( InformerEventSourceConfiguration configuration, EventSourceContext

context) { this( configuration, configuration.getKubernetesClient().orElse(context.getClient()), - context - .getControllerConfiguration() - .getConfigurationService() - .parseResourceVersionsForEventFilteringAndCaching()); + configuration.comparableResourceVersions()); } InformerEventSource(InformerEventSourceConfiguration configuration, KubernetesClient client) { - this(configuration, client, false); + this(configuration, client, DEFAULT_COMPARABLE_RESOURCE_VERSIONS); } @SuppressWarnings({"unchecked", "rawtypes"}) private InformerEventSource( InformerEventSourceConfiguration configuration, KubernetesClient client, - boolean parseResourceVersions) { + boolean comparableResourceVersions) { super( configuration.name(), configuration @@ -107,7 +105,7 @@ private InformerEventSource( .map(gvk -> client.genericKubernetesResources(gvk.apiVersion(), gvk.getKind())) .orElseGet(() -> (MixedOperation) client.resources(configuration.getResourceClass())), configuration, - parseResourceVersions); + comparableResourceVersions); // If there is a primary to secondary mapper there is no need for primary to secondary index. primaryToSecondaryMapper = configuration.getPrimaryToSecondaryMapper(); if (useSecondaryToPrimaryIndex()) { @@ -125,6 +123,22 @@ private InformerEventSource( genericFilter = informerConfig.getGenericFilter(); } + public R updateAndCacheResource( + R resourceToUpdate, Context context, UnaryOperator updateMethod) { + ResourceID id = ResourceID.fromResource(resourceToUpdate); + if (log.isDebugEnabled()) { + log.debug("Update and cache: {}", id); + } + try { + temporaryResourceCache.startModifying(id); + var updated = updateMethod.apply(resourceToUpdate); + handleRecentResourceUpdate(id, updated, resourceToUpdate); + return updated; + } finally { + temporaryResourceCache.doneModifying(id); + } + } + @Override public void onAdd(R newResource) { if (log.isDebugEnabled()) { @@ -134,9 +148,7 @@ public void onAdd(R newResource) { resourceType().getSimpleName(), newResource.getMetadata().getResourceVersion()); } - primaryToSecondaryIndex.onAddOrUpdate(newResource); - onAddOrUpdate( - Operation.ADD, newResource, null, () -> InformerEventSource.super.onAdd(newResource)); + onAddOrUpdate(Operation.ADD, newResource, null); } @Override @@ -149,16 +161,11 @@ public void onUpdate(R oldObject, R newObject) { newObject.getMetadata().getResourceVersion(), oldObject.getMetadata().getResourceVersion()); } - primaryToSecondaryIndex.onAddOrUpdate(newObject); - onAddOrUpdate( - Operation.UPDATE, - newObject, - oldObject, - () -> InformerEventSource.super.onUpdate(oldObject, newObject)); + onAddOrUpdate(Operation.UPDATE, newObject, oldObject); } @Override - public void onDelete(R resource, boolean b) { + public synchronized void onDelete(R resource, boolean b) { if (log.isDebugEnabled()) { log.debug( "On delete event received for resource id: {} type: {}", @@ -180,68 +187,28 @@ public synchronized void start() { manager().list().forEach(primaryToSecondaryIndex::onAddOrUpdate); } - private synchronized void onAddOrUpdate( - Operation operation, R newObject, R oldObject, Runnable superOnOp) { + private synchronized void onAddOrUpdate(Operation operation, R newObject, R oldObject) { + primaryToSecondaryIndex.onAddOrUpdate(newObject); var resourceID = ResourceID.fromResource(newObject); - if (canSkipEvent(newObject, oldObject, resourceID)) { + if (temporaryResourceCache.onAddOrUpdateEvent(newObject)) { log.debug( "Skipping event propagation for {}, since was a result of a reconcile action. Resource" + " ID: {}", operation, ResourceID.fromResource(newObject)); - superOnOp.run(); + } else if (eventAcceptedByFilter(operation, newObject, oldObject)) { + log.debug( + "Propagating event for {}, resource with same version not result of a reconciliation." + + " Resource ID: {}", + operation, + resourceID); + propagateEvent(newObject); } else { - superOnOp.run(); - if (eventAcceptedByFilter(operation, newObject, oldObject)) { - log.debug( - "Propagating event for {}, resource with same version not result of a reconciliation." - + " Resource ID: {}", - operation, - resourceID); - propagateEvent(newObject); - } else { - log.debug("Event filtered out for operation: {}, resourceID: {}", operation, resourceID); - } + log.debug("Event filtered out for operation: {}, resourceID: {}", operation, resourceID); } } - private boolean canSkipEvent(R newObject, R oldObject, ResourceID resourceID) { - var res = temporaryResourceCache.getResourceFromCache(resourceID); - if (res.isEmpty()) { - return isEventKnownFromAnnotation(newObject, oldObject); - } - boolean resVersionsEqual = - newObject - .getMetadata() - .getResourceVersion() - .equals(res.get().getMetadata().getResourceVersion()); - log.debug( - "Resource found in temporal cache for id: {} resource versions equal: {}", - resourceID, - resVersionsEqual); - return resVersionsEqual - || temporaryResourceCache.isLaterResourceVersion(resourceID, res.get(), newObject); - } - - private boolean isEventKnownFromAnnotation(R newObject, R oldObject) { - String previous = newObject.getMetadata().getAnnotations().get(PREVIOUS_ANNOTATION_KEY); - boolean known = false; - if (previous != null) { - String[] parts = previous.split(","); - if (id.equals(parts[0])) { - if (oldObject == null && parts.length == 1) { - known = true; - } else if (oldObject != null - && parts.length == 2 - && oldObject.getMetadata().getResourceVersion().equals(parts[1])) { - known = true; - } - } - } - return known; - } - private void propagateEvent(R object) { var primaryResourceIdSet = configuration().getSecondaryToPrimaryMapper().toPrimaryResourceIDs(object); @@ -289,23 +256,19 @@ public Set getSecondaryResources(P primary) { } @Override - public synchronized void handleRecentResourceUpdate( + public void handleRecentResourceUpdate( ResourceID resourceID, R resource, R previousVersionOfResource) { handleRecentCreateOrUpdate(Operation.UPDATE, resource, previousVersionOfResource); } @Override - public synchronized void handleRecentResourceCreate(ResourceID resourceID, R resource) { + public void handleRecentResourceCreate(ResourceID resourceID, R resource) { handleRecentCreateOrUpdate(Operation.ADD, resource, null); } private void handleRecentCreateOrUpdate(Operation operation, R newResource, R oldResource) { primaryToSecondaryIndex.onAddOrUpdate(newResource); - temporaryResourceCache.putResource( - newResource, - Optional.ofNullable(oldResource) - .map(r -> r.getMetadata().getResourceVersion()) - .orElse(null)); + temporaryResourceCache.putResource(newResource); } private boolean useSecondaryToPrimaryIndex() { @@ -333,22 +296,6 @@ private boolean acceptedByDeleteFilters(R resource, boolean b) { && (genericFilter == null || genericFilter.accept(resource)); } - /** - * Add an annotation to the resource so that the subsequent will be omitted - * - * @param resourceVersion null if there is no prior version - * @param target mutable resource that will be returned - */ - public R addPreviousAnnotation(String resourceVersion, R target) { - target - .getMetadata() - .getAnnotations() - .put( - PREVIOUS_ANNOTATION_KEY, - id + Optional.ofNullable(resourceVersion).map(rv -> "," + rv).orElse("")); - return target; - } - private enum Operation { ADD, UPDATE diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index 2a6c7ef206..c3a4a9f2c1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -156,6 +156,10 @@ public Optional get(ResourceID resourceID) { return Optional.ofNullable(cache.getByKey(getKey(resourceID))); } + public String getLastSyncResourceVersion() { + return this.informer.lastSyncResourceVersion(); + } + private String getKey(ResourceID resourceID) { return Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), resourceID.getName()); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 2679918b60..af30617d92 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -34,6 +34,7 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Informable; import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; +import io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils; import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; import io.javaoperatorsdk.operator.health.InformerHealthIndicator; import io.javaoperatorsdk.operator.health.InformerWrappingEventSourceHealthIndicator; @@ -55,7 +56,7 @@ public abstract class ManagedInformerEventSource< private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class); private InformerManager cache; - private final boolean parseResourceVersions; + private final boolean comparableResourceVersions; private ControllerConfiguration controllerConfiguration; private final C configuration; private final Map>> indexers = new HashMap<>(); @@ -63,9 +64,9 @@ public abstract class ManagedInformerEventSource< protected MixedOperation client; protected ManagedInformerEventSource( - String name, MixedOperation client, C configuration, boolean parseResourceVersions) { + String name, MixedOperation client, C configuration, boolean comparableResourceVersions) { super(configuration.getResourceClass(), name); - this.parseResourceVersions = parseResourceVersions; + this.comparableResourceVersions = comparableResourceVersions; this.client = client; this.configuration = configuration; } @@ -102,7 +103,7 @@ public synchronized void start() { if (isRunning()) { return; } - temporaryResourceCache = new TemporaryResourceCache<>(this, parseResourceVersions); + temporaryResourceCache = new TemporaryResourceCache<>(comparableResourceVersions); this.cache = new InformerManager<>(client, configuration, this); cache.setControllerConfiguration(controllerConfiguration); cache.addIndexers(indexers); @@ -122,30 +123,34 @@ public synchronized void stop() { @Override public void handleRecentResourceUpdate( ResourceID resourceID, R resource, R previousVersionOfResource) { - temporaryResourceCache.putResource( - resource, previousVersionOfResource.getMetadata().getResourceVersion()); + temporaryResourceCache.putResource(resource); } @Override public void handleRecentResourceCreate(ResourceID resourceID, R resource) { - temporaryResourceCache.putAddedResource(resource); + temporaryResourceCache.putResource(resource); } @Override public Optional get(ResourceID resourceID) { + var res = cache.get(resourceID); Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); - if (resource.isPresent()) { - log.debug("Resource found in temporary cache for Resource ID: {}", resourceID); + if (comparableResourceVersions + && resource.isPresent() + && res.filter( + r -> + PrimaryUpdateAndCacheUtils.compareResourceVersions(r, resource.orElseThrow()) + > 0) + .isEmpty()) { + log.debug("Latest resource found in temporary cache for Resource ID: {}", resourceID); return resource; - } else { - log.debug( - "Resource not found in temporary cache reading it from informer cache," - + " for Resource ID: {}", - resourceID); - var res = cache.get(resourceID); - log.debug("Resource found in cache: {} for id: {}", res.isPresent(), resourceID); - return res; } + log.debug( + "Resource not found, or older, in temporary cache. Found in informer cache {}, for" + + " Resource ID: {}", + res.isPresent(), + resourceID); + return res; } @SuppressWarnings("unused") diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 06226ae4ba..d918be447d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -15,16 +15,16 @@ */ package io.javaoperatorsdk.operator.processing.event.source.informer; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -33,157 +33,152 @@ * a create or update is executed the subsequent getResource operation might not return the * up-to-date resource from informer cache, since it is not received yet. * - *

The idea of the solution is, that since an update (for create is simpler) was done - * successfully, and optimistic locking is in place, there were no other operations between reading - * the resource from the cache and the actual update. So when the new resource is stored in the - * temporal cache only if the informer still has the previous resource version, from before the - * update. If not, that means there were already updates on the cache (either by the actual update - * from DependentResource or other) so the resource does not needs to be cached. Subsequently if - * event received from the informer, it means that the cache of the informer was updated, so it - * already contains a more fresh version of the resource. + *

Since an update (for create is simpler) was done successfully we can temporarily track that + * resource if its version is later than the events we've processed. We then know that we can skip + * all events that have the same resource version or earlier than the tracked resource. Once we + * process an event that has the same resource version or later, then we know the tracked resource + * can be removed. + * + *

In some cases it is possible for the informer to deliver events prior to the attempt to put + * the resource in the temporal cache. The startModifying/doneModifying methods are used to pause + * event delivery to ensure that temporal cache recognizes the put entry as an event that can be + * skipped. + * + *

If comparable resource versions are disabled, then this cache is effectively disabled. * * @param resource to cache. */ public class TemporaryResourceCache { - static class ExpirationCache { - private final LinkedHashMap cache; - private final int ttlMs; - - public ExpirationCache(int maxEntries, int ttlMs) { - this.ttlMs = ttlMs; - this.cache = - new LinkedHashMap<>() { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > maxEntries; - } - }; - } + private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); - public void add(K key) { - clean(); - cache.putIfAbsent(key, System.currentTimeMillis()); - } + private final Map cache = new ConcurrentHashMap<>(); + private final boolean comparableResourceVersions; + private final Map activelyModifying = new ConcurrentHashMap<>(); + private String latestResourceVersion; - public boolean contains(K key) { - clean(); - return cache.get(key) != null; - } + public TemporaryResourceCache(boolean comparableResourceVersions) { + this.comparableResourceVersions = comparableResourceVersions; + } - void clean() { - if (!cache.isEmpty()) { - long currentTimeMillis = System.currentTimeMillis(); - var iter = cache.entrySet().iterator(); - // the order will already be from oldest to newest, clean a fixed number of entries to - // amortize the cost amongst multiple calls - for (int i = 0; i < 10 && iter.hasNext(); i++) { - var entry = iter.next(); - if (currentTimeMillis - entry.getValue() > ttlMs) { - iter.remove(); - } - } - } + public void startModifying(ResourceID id) { + if (!comparableResourceVersions) { + return; } + activelyModifying + .compute( + id, + (ignored, lock) -> { + if (lock != null) { + throw new IllegalStateException(); // concurrent modifications to the same resource + // not allowed - this could be relaxed if needed + } + return new ReentrantLock(); + }) + .lock(); } - private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); - - private final Map cache = new ConcurrentHashMap<>(); - - // keep up to the last million deletions for up to 10 minutes - private final ExpirationCache tombstones = new ExpirationCache<>(1000000, 1200000); - private final ManagedInformerEventSource managedInformerEventSource; - private final boolean parseResourceVersions; - - public TemporaryResourceCache( - ManagedInformerEventSource managedInformerEventSource, - boolean parseResourceVersions) { - this.managedInformerEventSource = managedInformerEventSource; - this.parseResourceVersions = parseResourceVersions; + public void doneModifying(ResourceID id) { + if (!comparableResourceVersions) { + return; + } + activelyModifying.computeIfPresent( + id, + (ignored, lock) -> { + lock.unlock(); + return null; + }); } - public synchronized void onDeleteEvent(T resource, boolean unknownState) { - tombstones.add(resource.getMetadata().getUid()); + public void onDeleteEvent(T resource, boolean unknownState) { onEvent(resource, unknownState); } - public synchronized void onAddOrUpdateEvent(T resource) { - onEvent(resource, false); + /** + * @return true if the resourceVersion was already known + */ + public boolean onAddOrUpdateEvent(T resource) { + return onEvent(resource, false); } - synchronized void onEvent(T resource, boolean unknownState) { - cache.computeIfPresent( - ResourceID.fromResource(resource), - (id, cached) -> - (unknownState || !isLaterResourceVersion(id, cached, resource)) ? null : cached); + private boolean onEvent(T resource, boolean unknownState) { + ReentrantLock lock = activelyModifying.get(ResourceID.fromResource(resource)); + if (lock != null) { + lock.lock(); // wait for the modification to finish + lock.unlock(); // simply unlock as the event is guaranteed after the modification + } + boolean[] known = new boolean[1]; + synchronized (this) { + if (!unknownState) { + latestResourceVersion = resource.getMetadata().getResourceVersion(); + } + cache.computeIfPresent( + ResourceID.fromResource(resource), + (id, cached) -> { + boolean remove = unknownState; + if (!unknownState) { + int comp = PrimaryUpdateAndCacheUtils.compareResourceVersions(resource, cached); + if (comp >= 0) { + remove = true; + } + if (comp <= 0) { + known[0] = true; + } + } + if (remove) { + return null; + } + return cached; + }); + return known[0]; + } } - public synchronized void putAddedResource(T newResource) { - putResource(newResource, null); - } + /** put the item into the cache if it's for a later state than what has already been observed. */ + public synchronized void putResource(T newResource) { + if (!comparableResourceVersions) { + return; + } - /** - * put the item into the cache if the previousResourceVersion matches the current state. If not - * the currently cached item is removed. - * - * @param previousResourceVersion null indicates an add - */ - public synchronized void putResource(T newResource, String previousResourceVersion) { var resourceId = ResourceID.fromResource(newResource); - var cachedResource = managedInformerEventSource.get(resourceId).orElse(null); - - boolean moveAhead = false; - if (previousResourceVersion == null && cachedResource == null) { - if (tombstones.contains(newResource.getMetadata().getUid())) { - log.debug( - "Won't resurrect uid {} for resource id: {}", - newResource.getMetadata().getUid(), - resourceId); - return; - } - // we can skip further checks as this is a simple add and there's no previous entry to - // consider - moveAhead = true; + + if (newResource.getMetadata().getResourceVersion() == null) { + log.warn( + "Resource {}: with no resourceVersion put in temporary cache. This is not the expected" + + " usage pattern, only resources returned from the api server should be put in the" + + " cache.", + resourceId); + return; } - if (moveAhead - || (cachedResource != null - && (cachedResource - .getMetadata() - .getResourceVersion() - .equals(previousResourceVersion)) - || isLaterResourceVersion(resourceId, newResource, cachedResource))) { + // check against the latestResourceVersion processed by the TemporaryResourceCache + // If the resource is older, then we can safely ignore. + // + // this also prevents resurrecting recently deleted entities for which the delete event + // has already been processed + if (latestResourceVersion != null + && PrimaryUpdateAndCacheUtils.compareResourceVersions( + latestResourceVersion, newResource.getMetadata().getResourceVersion()) + > 0) { log.debug( - "Temporarily moving ahead to target version {} for resource id: {}", + "Resource {}: resourceVersion {} is not later than latest {}", + resourceId, newResource.getMetadata().getResourceVersion(), - resourceId); - cache.put(resourceId, newResource); - } else if (cache.remove(resourceId) != null) { - log.debug("Removed an obsolete resource from cache for id: {}", resourceId); + latestResourceVersion); + return; } - } - /** - * @return true if {@link ConfigurationService#parseResourceVersionsForEventFilteringAndCaching()} - * is enabled and the resourceVersion of newResource is numerically greater than - * cachedResource, otherwise false - */ - public boolean isLaterResourceVersion(ResourceID resourceId, T newResource, T cachedResource) { - try { - if (parseResourceVersions - && Long.parseLong(newResource.getMetadata().getResourceVersion()) - > Long.parseLong(cachedResource.getMetadata().getResourceVersion())) { - return true; - } - } catch (NumberFormatException e) { + // also make sure that we're later than the existing temporary entry + var cachedResource = getResourceFromCache(resourceId).orElse(null); + + if (cachedResource == null + || PrimaryUpdateAndCacheUtils.compareResourceVersions(newResource, cachedResource) > 0) { log.debug( - "Could not compare resourceVersions {} and {} for {}", + "Temporarily moving ahead to target version {} for resource id: {}", newResource.getMetadata().getResourceVersion(), - cachedResource.getMetadata().getResourceVersion(), resourceId); + cache.put(resourceId, newResource); } - return false; } public synchronized Optional getResourceFromCache(ResourceID resourceID) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 208d6aeaaa..f54e47304b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -94,31 +94,18 @@ public synchronized void start() {} } @Test - void skipsEventPropagationIfResourceWithSameVersionInResourceCache() { + void skipsEventPropagation() { when(temporaryResourceCacheMock.getResourceFromCache(any())) .thenReturn(Optional.of(testDeployment())); + when(temporaryResourceCacheMock.onAddOrUpdateEvent(any())).thenReturn(true); + informerEventSource.onAdd(testDeployment()); informerEventSource.onUpdate(testDeployment(), testDeployment()); verify(eventHandlerMock, never()).handleEvent(any()); } - @Test - void skipsAddEventPropagationViaAnnotation() { - informerEventSource.onAdd(informerEventSource.addPreviousAnnotation(null, testDeployment())); - - verify(eventHandlerMock, never()).handleEvent(any()); - } - - @Test - void skipsUpdateEventPropagationViaAnnotation() { - informerEventSource.onUpdate( - testDeployment(), informerEventSource.addPreviousAnnotation("1", testDeployment())); - - verify(eventHandlerMock, never()).handleEvent(any()); - } - @Test void processEventPropagationWithoutAnnotation() { informerEventSource.onUpdate(testDeployment(), testDeployment()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index e3dc2c82e4..4b12148015 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -16,10 +16,10 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.Map; -import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,49 +27,40 @@ import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.informer.TemporaryResourceCache.ExpirationCache; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; class TemporaryPrimaryResourceCacheTest { public static final String RESOURCE_VERSION = "2"; - @SuppressWarnings("unchecked") - private InformerEventSource informerEventSource; - private TemporaryResourceCache temporaryResourceCache; @BeforeEach void setup() { - informerEventSource = mock(InformerEventSource.class); - temporaryResourceCache = new TemporaryResourceCache<>(informerEventSource, false); + temporaryResourceCache = new TemporaryResourceCache<>(true); } @Test void updateAddsTheResourceIntoCacheIfTheInformerHasThePreviousResourceVersion() { var testResource = testResource(); var prevTestResource = testResource(); - prevTestResource.getMetadata().setResourceVersion("0"); - when(informerEventSource.get(any())).thenReturn(Optional.of(prevTestResource)); + prevTestResource.getMetadata().setResourceVersion("1"); - temporaryResourceCache.putResource(testResource, "0"); + temporaryResourceCache.putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isPresent(); } @Test - void updateNotAddsTheResourceIntoCacheIfTheInformerHasOtherVersion() { + void updateNotAddsTheResourceIntoCacheIfLaterVersionKnown() { var testResource = testResource(); - var informerCachedResource = testResource(); - informerCachedResource.getMetadata().setResourceVersion("x"); - when(informerEventSource.get(any())).thenReturn(Optional.of(informerCachedResource)); - temporaryResourceCache.putResource(testResource, "0"); + temporaryResourceCache.onAddOrUpdateEvent( + testResource.toBuilder().editMetadata().withResourceVersion("3").endMetadata().build()); + + temporaryResourceCache.putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isNotPresent(); @@ -78,9 +69,8 @@ void updateNotAddsTheResourceIntoCacheIfTheInformerHasOtherVersion() { @Test void addOperationAddsTheResourceIfInformerCacheStillEmpty() { var testResource = testResource(); - when(informerEventSource.get(any())).thenReturn(Optional.empty()); - temporaryResourceCache.putAddedResource(testResource); + temporaryResourceCache.putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isPresent(); @@ -89,46 +79,79 @@ void addOperationAddsTheResourceIfInformerCacheStillEmpty() { @Test void addOperationNotAddsTheResourceIfInformerCacheNotEmpty() { var testResource = testResource(); - when(informerEventSource.get(any())).thenReturn(Optional.of(testResource())); - temporaryResourceCache.putAddedResource(testResource); + temporaryResourceCache.putResource(testResource); + + temporaryResourceCache.putResource( + new ConfigMapBuilder(testResource) + .editMetadata() + .withResourceVersion("1") + .endMetadata() + .build()); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); - assertThat(cached).isNotPresent(); + assertThat(cached.orElseThrow().getMetadata().getResourceVersion()).isEqualTo(RESOURCE_VERSION); } @Test void removesResourceFromCache() { ConfigMap testResource = propagateTestResourceToCache(); - temporaryResourceCache.onAddOrUpdateEvent(testResource()); + temporaryResourceCache.onAddOrUpdateEvent( + new ConfigMapBuilder(testResource) + .editMetadata() + .withResourceVersion("3") + .endMetadata() + .build()); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isNotPresent(); } @Test - void resourceVersionParsing() { - this.temporaryResourceCache = new TemporaryResourceCache<>(informerEventSource, true); + void nonComparableResourceVersionsDisables() { + this.temporaryResourceCache = new TemporaryResourceCache<>(false); - ConfigMap testResource = propagateTestResourceToCache(); + this.temporaryResourceCache.putResource(testResource()); - // an event with a newer version will not remove - temporaryResourceCache.onAddOrUpdateEvent( - new ConfigMapBuilder(testResource) - .editMetadata() - .withResourceVersion("1") - .endMetadata() - .build()); + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource()))) + .isEmpty(); + } - assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) - .isPresent(); + @Test + void lockedEventBeforePut() throws Exception { + var testResource = testResource(); - // anything else will remove - temporaryResourceCache.onAddOrUpdateEvent(testResource()); + temporaryResourceCache.startModifying(ResourceID.fromResource(testResource)); - assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) - .isNotPresent(); + ExecutorService ex = Executors.newSingleThreadExecutor(); + try { + var result = ex.submit(() -> temporaryResourceCache.onAddOrUpdateEvent(testResource)); + + temporaryResourceCache.putResource(testResource); + assertThat(result.isDone()).isFalse(); + temporaryResourceCache.doneModifying(ResourceID.fromResource(testResource)); + assertThat(result.get(10, TimeUnit.SECONDS)).isTrue(); + } finally { + ex.shutdownNow(); + } + } + + @Test + void putBeforeEvent() { + var testResource = testResource(); + + // first ensure an event is not known + var result = temporaryResourceCache.onAddOrUpdateEvent(testResource); + assertThat(result).isFalse(); + + var nextResource = testResource(); + nextResource.getMetadata().setResourceVersion("3"); + temporaryResourceCache.putResource(nextResource); + + // now expect an event with the matching resourceVersion to be known after the put + result = temporaryResourceCache.onAddOrUpdateEvent(nextResource); + assertThat(result).isTrue(); } @Test @@ -143,45 +166,15 @@ void rapidDeletion() { .endMetadata() .build(), false); - temporaryResourceCache.putAddedResource(testResource); + temporaryResourceCache.putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isEmpty(); } - @Test - void expirationCacheMax() { - ExpirationCache cache = new ExpirationCache<>(2, Integer.MAX_VALUE); - - cache.add(1); - cache.add(2); - cache.add(3); - - assertThat(cache.contains(1)).isFalse(); - assertThat(cache.contains(2)).isTrue(); - assertThat(cache.contains(3)).isTrue(); - } - - @Test - void expirationCacheTtl() { - ExpirationCache cache = new ExpirationCache<>(2, 1); - - cache.add(1); - cache.add(2); - - Awaitility.await() - .atMost(1, TimeUnit.SECONDS) - .untilAsserted( - () -> { - assertThat(cache.contains(1)).isFalse(); - assertThat(cache.contains(2)).isFalse(); - }); - } - private ConfigMap propagateTestResourceToCache() { var testResource = testResource(); - when(informerEventSource.get(any())).thenReturn(Optional.empty()); - temporaryResourceCache.putAddedResource(testResource); + temporaryResourceCache.putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isPresent(); return testResource; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/PreviousAnnotationDisabledIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/ComparableResourceVersionsDisabledIT.java similarity index 100% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/PreviousAnnotationDisabledIT.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/ComparableResourceVersionsDisabledIT.java diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java index de485cfc4e..89d1dee94b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java @@ -104,13 +104,15 @@ private void createExternalResource( .withData(Map.of(ID_KEY, createdResource.getId())) .build(); configMap.addOwnerReference(resource); - context.getClient().configMaps().resource(configMap).create(); var primaryID = ResourceID.fromResource(resource); // Making sure that the created resources are in the cache for the next reconciliation. // This is critical in this case, since on next reconciliation if it would not be in the cache // it would be created again. - configMapEventSource.handleRecentResourceCreate(primaryID, configMap); + configMapEventSource.updateAndCacheResource( + configMap, + context, + toCreate -> context.getClient().configMaps().resource(toCreate).create()); externalResourceEventSource.handleRecentResourceCreate(primaryID, createdResource); } @@ -128,6 +130,7 @@ public DeleteControl cleanup( return DeleteControl.defaultDelete(); } + @Override public int getNumberOfExecutions() { return numberOfExecutions.get(); } From fb32b02f52d4b090c23e3713407da700c8ce83ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 13 Nov 2025 04:43:22 +0100 Subject: [PATCH 04/26] improve: complete comparable resource version configs (#3027) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/ConfigurationService.java | 15 ----------- .../config/ConfigurationServiceOverrider.java | 27 ------------------- .../api/config/informer/Informer.java | 8 ++++++ .../informer/InformerConfiguration.java | 18 +++++++++++-- .../InformerEventSourceConfiguration.java | 22 +++++++-------- .../operator/api/reconciler/Constants.java | 2 +- .../controller/ControllerEventSource.java | 2 +- .../source/informer/InformerEventSource.java | 6 ++--- .../ComparableResourceVersionsDisabledIT.java | 4 +-- ...CreateUpdateEventFilterTestReconciler.java | 11 ++++++++ 10 files changed, 52 insertions(+), 63 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 99bb280ae4..6ed9b7ff64 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -44,8 +44,6 @@ import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSIONS; - /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { @@ -447,19 +445,6 @@ default Set> defaultNonSSAResource() { return defaultNonSSAResources(); } - /** - * If the event logic can compare resourceVersions. - * - *

Enabled by default as Kubernetes does support this interpretation of resourceVersions. - * Disable only if your api server provides non comparable resource versions. - * - * @return if resource versions are comparable - * @since 5.3.0 - */ - default boolean comparableResourceVersions() { - return DEFAULT_COMPARABLE_RESOURCE_VERSIONS; - } - /** * {@link io.javaoperatorsdk.operator.api.reconciler.UpdateControl} patch resource or status can * either use simple patches or SSA. Setting this to {@code true}, controllers will use SSA for diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index 81a5428044..cd9cdafb39 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -51,7 +51,6 @@ public class ConfigurationServiceOverrider { private Duration reconciliationTerminationTimeout; private Boolean ssaBasedCreateUpdateMatchForDependentResources; private Set> defaultNonSSAResource; - private Boolean comparableResourceVersions; private Boolean useSSAToPatchPrimaryResource; private Boolean cloneSecondaryResourcesWhenGettingFromCache; @@ -166,26 +165,6 @@ public ConfigurationServiceOverrider withDefaultNonSSAResource( return this; } - /** - * @param value true if internal algorithms can use metadata.resourceVersion as a numeric value. - * @return this - */ - public ConfigurationServiceOverrider withComparableResourceVersions(boolean value) { - this.comparableResourceVersions = value; - return this; - } - - /** - * @deprecated use withComparableResourceVersions - * @param value true if internal algorithms can use metadata.resourceVersion as a numeric value. - * @return this - */ - @Deprecated(forRemoval = true) - public ConfigurationServiceOverrider withParseResourceVersions(boolean value) { - this.comparableResourceVersions = value; - return this; - } - public ConfigurationServiceOverrider withUseSSAToPatchPrimaryResource(boolean value) { this.useSSAToPatchPrimaryResource = value; return this; @@ -330,12 +309,6 @@ public boolean cloneSecondaryResourcesWhenGettingFromCache() { cloneSecondaryResourcesWhenGettingFromCache, ConfigurationService::cloneSecondaryResourcesWhenGettingFromCache); } - - @Override - public boolean comparableResourceVersions() { - return overriddenValueOrDefault( - comparableResourceVersions, ConfigurationService::comparableResourceVersions); - } }; } } 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 9264db66bc..e6655641a2 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 @@ -28,6 +28,7 @@ import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION; import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_LONG_VALUE_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_VALUE_SET; @@ -131,4 +132,11 @@ /** Kubernetes field selector for additional resource filtering */ Field[] fieldSelector() default {}; + + /** + * true if we can consider resource versions as integers, therefore it is valid to compare them + * + * @since 5.3.0 + */ + boolean comparableResourceVersions() default DEFAULT_COMPARABLE_RESOURCE_VERSION; } 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 24f78eb7be..30a1a32e8a 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 @@ -53,6 +53,7 @@ public class InformerConfiguration { private ItemStore itemStore; private Long informerListLimit; private FieldSelector fieldSelector; + private boolean comparableResourceVersions; protected InformerConfiguration( Class resourceClass, @@ -66,7 +67,8 @@ protected InformerConfiguration( GenericFilter genericFilter, ItemStore itemStore, Long informerListLimit, - FieldSelector fieldSelector) { + FieldSelector fieldSelector, + boolean comparableResourceVersions) { this(resourceClass); this.name = name; this.namespaces = namespaces; @@ -79,6 +81,7 @@ protected InformerConfiguration( this.itemStore = itemStore; this.informerListLimit = informerListLimit; this.fieldSelector = fieldSelector; + this.comparableResourceVersions = comparableResourceVersions; } private InformerConfiguration(Class resourceClass) { @@ -113,7 +116,8 @@ public static InformerConfiguration.Builder builder( original.genericFilter, original.itemStore, original.informerListLimit, - original.fieldSelector) + original.fieldSelector, + original.comparableResourceVersions) .builder; } @@ -288,6 +292,10 @@ public FieldSelector getFieldSelector() { return fieldSelector; } + public boolean isComparableResourceVersions() { + return comparableResourceVersions; + } + @SuppressWarnings("UnusedReturnValue") public class Builder { @@ -359,6 +367,7 @@ public InformerConfiguration.Builder initFromAnnotation( Arrays.stream(informerConfig.fieldSelector()) .map(f -> new FieldSelector.Field(f.path(), f.value(), f.negated())) .toList())); + withComparableResourceVersions(informerConfig.comparableResourceVersions()); } return this; } @@ -459,5 +468,10 @@ public Builder withFieldSelector(FieldSelector fieldSelector) { InformerConfiguration.this.fieldSelector = fieldSelector; return this; } + + public Builder withComparableResourceVersions(boolean comparableResourceVersions) { + InformerConfiguration.this.comparableResourceVersions = comparableResourceVersions; + 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 c6ea21f0c0..69903e805f 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 @@ -33,7 +33,7 @@ import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSIONS; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION; import static io.javaoperatorsdk.operator.api.reconciler.Constants.SAME_AS_CONTROLLER_NAMESPACES_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_ALL_NAMESPACE_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE_SET; @@ -97,7 +97,7 @@ class DefaultInformerEventSourceConfiguration private final GroupVersionKind groupVersionKind; private final InformerConfiguration informerConfig; private final KubernetesClient kubernetesClient; - private final boolean comparableResourceVersions; + private final boolean comparableResourceVersion; protected DefaultInformerEventSourceConfiguration( GroupVersionKind groupVersionKind, @@ -105,13 +105,13 @@ protected DefaultInformerEventSourceConfiguration( SecondaryToPrimaryMapper secondaryToPrimaryMapper, InformerConfiguration informerConfig, KubernetesClient kubernetesClient, - boolean comparableResourceVersions) { + boolean comparableResourceVersion) { this.informerConfig = Objects.requireNonNull(informerConfig); this.groupVersionKind = groupVersionKind; this.primaryToSecondaryMapper = primaryToSecondaryMapper; this.secondaryToPrimaryMapper = secondaryToPrimaryMapper; this.kubernetesClient = kubernetesClient; - this.comparableResourceVersions = comparableResourceVersions; + this.comparableResourceVersion = comparableResourceVersion; } @Override @@ -141,8 +141,8 @@ public Optional getKubernetesClient() { } @Override - public boolean comparableResourceVersions() { - return this.comparableResourceVersions; + public boolean comparableResourceVersion() { + return this.comparableResourceVersion; } } @@ -157,7 +157,7 @@ class Builder { private PrimaryToSecondaryMapper primaryToSecondaryMapper; private SecondaryToPrimaryMapper secondaryToPrimaryMapper; private KubernetesClient kubernetesClient; - private boolean comparableResourceVersions = DEFAULT_COMPARABLE_RESOURCE_VERSIONS; + private boolean comparableResourceVersion = DEFAULT_COMPARABLE_RESOURCE_VERSION; private Builder(Class resourceClass, Class primaryResourceClass) { this(resourceClass, primaryResourceClass, null); @@ -295,8 +295,8 @@ public Builder withFieldSelector(FieldSelector fieldSelector) { return this; } - public Builder withComparableResourceVersions(boolean comparableResourceVersions) { - this.comparableResourceVersions = comparableResourceVersions; + public Builder withComparableResourceVersion(boolean comparableResourceVersion) { + this.comparableResourceVersion = comparableResourceVersion; return this; } @@ -340,9 +340,9 @@ public InformerEventSourceConfiguration build() { false)), config.build(), kubernetesClient, - comparableResourceVersions); + comparableResourceVersion); } } - boolean comparableResourceVersions(); + boolean comparableResourceVersion(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java index ed975d71ef..7330a407c1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java @@ -41,7 +41,7 @@ public final class Constants { public static final String RESOURCE_GVK_KEY = "josdk.resource.gvk"; public static final String CONTROLLER_NAME = "controller.name"; public static final boolean DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES = true; - public static final boolean DEFAULT_COMPARABLE_RESOURCE_VERSIONS = true; + public static final boolean DEFAULT_COMPARABLE_RESOURCE_VERSION = true; private Constants() {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java index 59d86efe48..f7ed9fdc8e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java @@ -51,7 +51,7 @@ public ControllerEventSource(Controller controller) { NAME, controller.getCRClient(), controller.getConfiguration(), - controller.getConfiguration().getConfigurationService().comparableResourceVersions()); + controller.getConfiguration().getInformerConfig().isComparableResourceVersions()); this.controller = controller; final var config = controller.getConfiguration(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index d46dd0669a..c6a0c782e3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -35,7 +35,7 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSIONS; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION; /** * Wraps informer(s) so they are connected to the eventing system of the framework. Note that since @@ -86,11 +86,11 @@ public InformerEventSource( this( configuration, configuration.getKubernetesClient().orElse(context.getClient()), - configuration.comparableResourceVersions()); + configuration.comparableResourceVersion()); } InformerEventSource(InformerEventSourceConfiguration configuration, KubernetesClient client) { - this(configuration, client, DEFAULT_COMPARABLE_RESOURCE_VERSIONS); + this(configuration, client, DEFAULT_COMPARABLE_RESOURCE_VERSION); } @SuppressWarnings({"unchecked", "rawtypes"}) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/ComparableResourceVersionsDisabledIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/ComparableResourceVersionsDisabledIT.java index 17fe6b7125..6577d4ca59 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/ComparableResourceVersionsDisabledIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/ComparableResourceVersionsDisabledIT.java @@ -34,9 +34,7 @@ class PreviousAnnotationDisabledIT { @RegisterExtension LocallyRunOperatorExtension operator = LocallyRunOperatorExtension.builder() - .withReconciler(new CreateUpdateEventFilterTestReconciler()) - .withConfigurationService( - overrider -> overrider.withPreviousAnnotationForDependentResources(false)) + .withReconciler(new CreateUpdateEventFilterTestReconciler(false)) .build(); @Test diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java index 40bf2cc350..4344356ff9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java @@ -41,6 +41,16 @@ public class CreateUpdateEventFilterTestReconciler private final DirectConfigMapDependentResource configMapDR = new DirectConfigMapDependentResource(ConfigMap.class); + private final boolean comparableResourceVersion; + + public CreateUpdateEventFilterTestReconciler(boolean comparableResourceVersion) { + this.comparableResourceVersion = comparableResourceVersion; + } + + public CreateUpdateEventFilterTestReconciler() { + this(true); + } + @Override public UpdateControl reconcile( CreateUpdateEventFilterTestCustomResource resource, @@ -89,6 +99,7 @@ public List> prepareEv InformerEventSourceConfiguration.from( ConfigMap.class, CreateUpdateEventFilterTestCustomResource.class) .withLabelSelector("integrationtest = " + this.getClass().getSimpleName()) + .withComparableResourceVersion(comparableResourceVersion) .build(); final var informerEventSource = new InformerEventSource<>(informerConfiguration, context); From 1530d1f8fea58e4086fbf4b699ae55c4363287d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 17 Nov 2025 17:39:11 +0100 Subject: [PATCH 05/26] improve: run pr-s checks for v5.3 (#3042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .github/workflows/e2e-test.yml | 3 +- .github/workflows/pr.yml | 2 +- .../PrimaryUpdateAndCacheUtilsTest.java | 49 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index a637c72927..172f28e3f9 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -6,7 +6,7 @@ on: paths-ignore: - 'docs/**' - 'adr/**' - branches: [ main, next ] + branches: [ main, next, v5.3 ] push: paths-ignore: - 'docs/**' @@ -14,6 +14,7 @@ on: branches: - main - next + - v5.3 jobs: sample_operators_tests: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e889a0be13..a6bd5c2032 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -11,7 +11,7 @@ on: paths-ignore: - 'docs/**' - 'adr/**' - branches: [ main, v1, v2, v3, next ] + branches: [ main, v1, v2, v3, next, v5.3 ] workflow_dispatch: jobs: check_format_and_unit_tests: diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java index 235dd3cd40..1a009eb57c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java @@ -180,4 +180,53 @@ void cachePollTimeouts() { 10L)); assertThat(ex.getMessage()).contains("Timeout"); } + + @Test + public void compareResourceVersionsTest() { + assertThat(compareResourceVersions("11", "22")).isNegative(); + assertThat(compareResourceVersions("22", "11")).isPositive(); + assertThat(compareResourceVersions("1", "1")).isZero(); + assertThat(compareResourceVersions("11", "11")).isZero(); + assertThat(compareResourceVersions("123", "2")).isPositive(); + assertThat(compareResourceVersions("3", "211")).isNegative(); + + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("aa", "22")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("11", "ba")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("", "22")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("11", "")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("01", "123")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("123", "01")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("3213", "123a")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("321", "123a")); + } + + // naive performance test that compares the work case scenario for the parsing and non-parsing + // variants + @Test + @Disabled + public void compareResourcePerformanceTest() { + var execNum = 30000000; + var startTime = System.currentTimeMillis(); + for (int i = 0; i < execNum; i++) { + var res = compareResourceVersions("123456788", "123456789"); + } + var dur1 = System.currentTimeMillis() - startTime; + log.info("Duration without parsing: {}", dur1); + startTime = System.currentTimeMillis(); + for (int i = 0; i < execNum; i++) { + var res = Long.parseLong("123456788") > Long.parseLong("123456789"); + } + var dur2 = System.currentTimeMillis() - startTime; + log.info("Duration with parsing: {}", dur2); + + assertThat(dur1).isLessThan(dur2); + } } From f4b4024f65a608008716bd5aaa157050122ea435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Dec 2025 11:17:06 +0100 Subject: [PATCH 06/26] fix: rebase on main after release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/source/CacheKeyMapper.java | 15 +++++++++++++++ .../PrimaryUpdateAndCacheUtilsTest.java | 2 ++ test-index-processor/pom.xml | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java index e69de29bb2..3e1a4f9b14 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java @@ -0,0 +1,15 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java index 1a009eb57c..c878a4fc06 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java @@ -19,6 +19,7 @@ import java.util.function.UnaryOperator; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,6 +40,7 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils.DEFAULT_MAX_RETRY; +import static io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils.compareResourceVersions; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; diff --git a/test-index-processor/pom.xml b/test-index-processor/pom.xml index 3dad709023..5ea4008f78 100644 --- a/test-index-processor/pom.xml +++ b/test-index-processor/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT test-index-processor From cb39807e716ea34d658be568e9f115844b4b7a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Dec 2025 15:53:03 +0100 Subject: [PATCH 07/26] fix(javadoc): invalid method ref blocks snapshot release (#3076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit simplified the javadoc Signed-off-by: Attila Mészáros --- .../source/informer/InformerEventSource.java | 29 ++----------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index c6a0c782e3..0feb3dc2a8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -40,33 +40,8 @@ /** * Wraps informer(s) so they are connected to the eventing system of the framework. Note that since * this is built on top of Fabric8 client Informers, it also supports caching resources using - * caching from informer caches as well as additional caches described below. - * - *

InformerEventSource also supports two features to better handle events and caching of - * resources on top of Informers from the Fabric8 Kubernetes client. These two features are related - * to each other as follows: - * - *

    - *
  1. Ensuring the cache contains the fresh resource after an update. This is important for - * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} and mainly - * for {@link - * io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource} so - * that {@link - * io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource#getSecondaryResource(HasMetadata, - * Context)} always returns the latest version of the resource after a reconciliation. To - * achieve this {@link #handleRecentResourceUpdate(ResourceID, HasMetadata, HasMetadata)} and - * {@link #handleRecentResourceCreate(ResourceID, HasMetadata)} need to be called explicitly - * after a resource is created or updated using the kubernetes client. These calls are done - * automatically by the KubernetesDependentResource implementation. In the background this - * will store the new resource in a temporary cache {@link TemporaryResourceCache} which does - * additional checks. After a new event is received the cached object is removed from this - * cache, since it is then usually already in the informer cache. - *
  2. Avoiding unneeded reconciliations after resources are created or updated. This filters out - * events that are the results of updates and creates made by the controller itself because we - * typically don't want the associated informer to trigger an event causing a useless - * reconciliation (as the change originates from the reconciler itself). For the details see - * {@link #canSkipEvent(HasMetadata, HasMetadata, ResourceID)} and related usage. - *
+ * caching from informer caches as well as filtering events which are result of the controller's + * update. * * @param resource type being watched * @param

type of the associated primary resource From 0d7f0262ae9448b9b20d3067133b65ea084750c1 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 3 Dec 2025 09:47:12 +0100 Subject: [PATCH 08/26] feat: record desired state in Context (#3082) Signed-off-by: Chris Laprun --- .../api/reconciler/DefaultContext.java | 13 +++ .../dependent/AbstractDependentResource.java | 28 +++++- .../AbstractExternalDependentResource.java | 2 +- .../BulkDependentResourceReconciler.java | 1 - .../GenericKubernetesResourceMatcher.java | 4 +- .../KubernetesDependentResource.java | 8 +- .../AbstractDependentResourceTest.java | 87 ++++++++++++++----- .../GenericKubernetesResourceMatcherTest.java | 32 ++++--- 8 files changed, 134 insertions(+), 41 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index f3fade4659..f1aeadd52a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -15,15 +15,19 @@ */ package io.javaoperatorsdk.operator.api.reconciler; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedWorkflowAndDependentResourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; @@ -41,6 +45,7 @@ public class DefaultContext

implements Context

{ defaultManagedDependentResourceContext; private final boolean primaryResourceDeleted; private final boolean primaryResourceFinalStateUnknown; + private final Map, Object> desiredStates = new ConcurrentHashMap<>(); public DefaultContext( RetryInfo retryInfo, @@ -157,4 +162,12 @@ public DefaultContext

setRetryInfo(RetryInfo retryInfo) { this.retryInfo = retryInfo; return this; } + + @SuppressWarnings("unchecked") + public R getOrComputeDesiredStateFor( + DependentResource dependentResource, Function desiredStateComputer) { + return (R) + desiredStates.computeIfAbsent( + dependentResource, ignored -> desiredStateComputer.apply(getPrimaryResource())); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java index a7c5ce9e2d..8dc62b4ca7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java @@ -23,6 +23,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @@ -85,7 +86,7 @@ protected ReconcileResult reconcile(P primary, R actualResource, Context

c if (creatable() || updatable()) { if (actualResource == null) { if (creatable) { - var desired = desired(primary, context); + var desired = getOrComputeDesired(context); throwIfNull(desired, primary, "Desired"); logForOperation("Creating", primary, desired); var createdResource = handleCreate(desired, primary, context); @@ -95,7 +96,8 @@ protected ReconcileResult reconcile(P primary, R actualResource, Context

c if (updatable()) { final Matcher.Result match = match(actualResource, primary, context); if (!match.matched()) { - final var desired = match.computedDesired().orElseGet(() -> desired(primary, context)); + final var desired = + match.computedDesired().orElseGet(() -> getOrComputeDesired(context)); throwIfNull(desired, primary, "Desired"); logForOperation("Updating", primary, desired); var updatedResource = handleUpdate(actualResource, desired, primary, context); @@ -127,7 +129,6 @@ protected ReconcileResult reconcile(P primary, R actualResource, Context

c @Override public Optional getSecondaryResource(P primary, Context

context) { - var secondaryResources = context.getSecondaryResources(resourceType()); if (secondaryResources.isEmpty()) { return Optional.empty(); @@ -212,6 +213,27 @@ protected R desired(P primary, Context

context) { + " updated"); } + /** + * Retrieves the desired state from the {@link Context} if it has already been computed or calls + * {@link #desired(HasMetadata, Context)} and stores its result in the context for further use. + * This ensures that {@code desired} is only called once per reconciliation to avoid unneeded + * processing and supports scenarios where idempotent computation of the desired state is not + * feasible. + * + *

Note that this method should normally only be called by the SDK itself and exclusively (i.e. + * {@link #desired(HasMetadata, Context)} should not be called directly by the SDK) whenever the + * desired state is needed to ensure it is properly cached for the current reconciliation. + * + * @param context the {@link Context} in scope for the current reconciliation + * @return the desired state associated with this dependent resource based on the currently + * in-scope primary resource as found in the context + */ + protected R getOrComputeDesired(Context

context) { + assert context instanceof DefaultContext

; + DefaultContext

defaultContext = (DefaultContext

) context; + return defaultContext.getOrComputeDesiredStateFor(this, p -> desired(p, defaultContext)); + } + public void delete(P primary, Context

context) { dependentResourceReconciler.delete(primary, context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java index e601e937cf..7b83a377c1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java @@ -105,7 +105,7 @@ protected void handleExplicitStateCreation(P primary, R created, Context

cont @Override public Matcher.Result match(R resource, P primary, Context

context) { - var desired = desired(primary, context); + var desired = getOrComputeDesired(context); return Matcher.Result.computed(resource.equals(desired), desired); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java index 5b3617c26c..23135f81b1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java @@ -27,7 +27,6 @@ import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; -import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; class BulkDependentResourceReconciler implements DependentResourceReconciler { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index 0ba48797af..5562c883e2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -138,7 +138,7 @@ public static Matcher.Result m Context

context, boolean labelsAndAnnotationsEquality, String... ignorePaths) { - final var desired = dependentResource.desired(primary, context); + final var desired = dependentResource.getOrComputeDesired(context); return match(desired, actualResource, labelsAndAnnotationsEquality, context, ignorePaths); } @@ -150,7 +150,7 @@ public static Matcher.Result m boolean specEquality, boolean labelsAndAnnotationsEquality, String... ignorePaths) { - final var desired = dependentResource.desired(primary, context); + final var desired = dependentResource.getOrComputeDesired(context); return match( desired, actualResource, labelsAndAnnotationsEquality, specEquality, context, ignorePaths); } 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 562a6257b5..5d53b807cc 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 @@ -143,7 +143,7 @@ public R update(R actual, R desired, P primary, Context

context) { @Override public Result match(R actualResource, P primary, Context

context) { - final var desired = desired(primary, context); + final var desired = getOrComputeDesired(context); return match(actualResource, desired, primary, context); } @@ -297,7 +297,7 @@ protected Optional selectTargetSecondaryResource( * @return id of the target managed resource */ protected ResourceID targetSecondaryResourceID(P primary, Context

context) { - return ResourceID.fromResource(desired(primary, context)); + return ResourceID.fromResource(getOrComputeDesired(context)); } protected boolean addOwnerReference() { @@ -305,8 +305,8 @@ protected boolean addOwnerReference() { } @Override - protected R desired(P primary, Context

context) { - return super.desired(primary, context); + protected R getOrComputeDesired(Context

context) { + return super.getOrComputeDesired(context); } @Override diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java index bb9d6cf71e..1db69a1f9e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java @@ -21,8 +21,10 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.junit.jupiter.api.Assertions.*; @@ -31,6 +33,13 @@ class AbstractDependentResourceTest { + private static final TestCustomResource PRIMARY = new TestCustomResource(); + private static final DefaultContext CONTEXT = createContext(PRIMARY); + + private static DefaultContext createContext(TestCustomResource primary) { + return new DefaultContext<>(mock(), mock(), primary, false, false); + } + @Test void throwsExceptionIfDesiredIsNullOnCreate() { TestDependentResource testDependentResource = new TestDependentResource(); @@ -38,8 +47,7 @@ void throwsExceptionIfDesiredIsNullOnCreate() { testDependentResource.setDesired(null); assertThrows( - DependentResourceException.class, - () -> testDependentResource.reconcile(new TestCustomResource(), null)); + DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT)); } @Test @@ -49,8 +57,7 @@ void throwsExceptionIfDesiredIsNullOnUpdate() { testDependentResource.setDesired(null); assertThrows( - DependentResourceException.class, - () -> testDependentResource.reconcile(new TestCustomResource(), null)); + DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT)); } @Test @@ -60,8 +67,7 @@ void throwsExceptionIfCreateReturnsNull() { testDependentResource.setDesired(configMap()); assertThrows( - DependentResourceException.class, - () -> testDependentResource.reconcile(new TestCustomResource(), null)); + DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT)); } @Test @@ -71,8 +77,28 @@ void throwsExceptionIfUpdateReturnsNull() { testDependentResource.setDesired(configMap()); assertThrows( - DependentResourceException.class, - () -> testDependentResource.reconcile(new TestCustomResource(), null)); + DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT)); + } + + @Test + void checkThatDesiredIsOnlyCalledOnce() { + final var testDependentResource = new DesiredCallCountCheckingDR(); + final var primary = new TestCustomResource(); + final var spec = primary.getSpec(); + spec.setConfigMapName("foo"); + spec.setKey("key"); + spec.setValue("value"); + final var context = createContext(primary); + testDependentResource.reconcile(primary, context); + + spec.setValue("value2"); + testDependentResource.reconcile(primary, context); + + assertEquals(1, testDependentResource.desiredCallCount); + + context.getOrComputeDesiredStateFor( + testDependentResource, p -> testDependentResource.desired(p, context)); + assertEquals(1, testDependentResource.desiredCallCount); } private ConfigMap configMap() { @@ -130,22 +156,12 @@ protected ConfigMap desired(TestCustomResource primary, Context match( return result; } } + + private static class DesiredCallCountCheckingDR extends TestDependentResource { + private short desiredCallCount; + + @Override + public ConfigMap update( + ConfigMap actual, + ConfigMap desired, + TestCustomResource primary, + Context context) { + return desired; + } + + @Override + public ConfigMap create( + ConfigMap desired, TestCustomResource primary, Context context) { + return desired; + } + + @Override + protected ConfigMap desired(TestCustomResource primary, Context context) { + final var spec = primary.getSpec(); + desiredCallCount++; + return new ConfigMapBuilder() + .editOrNewMetadata() + .withName(spec.getConfigMapName()) + .endMetadata() + .addToData(spec.getKey(), spec.getValue()) + .build(); + } + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 495fe98416..8a920b28b9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -18,37 +18,48 @@ import java.util.Map; import java.util.Optional; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.api.model.apps.DeploymentStatusBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesResourceMatcher.match; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; @SuppressWarnings({"unchecked"}) class GenericKubernetesResourceMatcherTest { - private static final Context context = mock(Context.class); + private static final Context context = new TestContext(); + + private static class TestContext extends DefaultContext { + private final KubernetesClient client = MockKubernetesClient.client(HasMetadata.class); + + public TestContext() { + this(null); + } + + public TestContext(HasMetadata primary) { + super(mock(), mock(), primary, false, false); + } + + @Override + public KubernetesClient getClient() { + return client; + } + } Deployment actual = createDeployment(); Deployment desired = createDeployment(); TestDependentResource dependentResource = new TestDependentResource(desired); - @BeforeAll - static void setUp() { - final var client = MockKubernetesClient.client(HasMetadata.class); - when(context.getClient()).thenReturn(client); - } - @Test void matchesTrivialCases() { assertThat(GenericKubernetesResourceMatcher.match(desired, actual, context).matched()).isTrue(); @@ -77,9 +88,10 @@ void matchesWithStrongSpecEquality() { @Test void doesNotMatchRemovedValues() { actual = createDeployment(); + final var localContext = new TestContext(createPrimary("removed")); assertThat( GenericKubernetesResourceMatcher.match( - dependentResource.desired(createPrimary("removed"), null), actual, context) + dependentResource.getOrComputeDesired(localContext), actual, localContext) .matched()) .withFailMessage("Removing values in metadata should lead to a mismatch") .isFalse(); From 578b5e79487eec3f9415efcd555d8a087ae30693 Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Thu, 30 Oct 2025 08:22:09 -0400 Subject: [PATCH 09/26] Annotation removal using locking (#3015) Signed-off-by: Steve Hawkins --- .../operator/api/config/ConfigurationService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 6ed9b7ff64..90b8ebaf7b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -44,6 +44,8 @@ import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSIONS; + /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { From f2dfd4850281b16be70d74a1d42ed06b1bbc35c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 13 Nov 2025 04:43:22 +0100 Subject: [PATCH 10/26] improve: complete comparable resource version configs (#3027) 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/ConfigurationService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 90b8ebaf7b..6ed9b7ff64 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -44,8 +44,6 @@ import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSIONS; - /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { From 4328a4ecf801cceb1a98849d9efd69ab7c40097d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 14 Oct 2025 17:29:36 +0200 Subject: [PATCH 11/26] chore: change version to 5.2.0-SNAPSHOT (#2995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- bootstrapper-maven-plugin/pom.xml | 2 +- micrometer-support/pom.xml | 2 +- operator-framework-bom/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- sample-operators/leader-election/pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml index 364495fca9..0ae72b2ff3 100644 --- a/bootstrapper-maven-plugin/pom.xml +++ b/bootstrapper-maven-plugin/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.3.0-SNAPSHOT + 5.2.0-SNAPSHOT bootstrapper diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 93765e4a8a..319eac7453 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.3.0-SNAPSHOT + 5.2.0-SNAPSHOT micrometer-support diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index 1ffa1701f8..010934a7cc 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk operator-framework-bom - 5.3.0-SNAPSHOT + 5.2.0-SNAPSHOT pom Operator SDK - Bill of Materials Java SDK for implementing Kubernetes operators diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 361bb3ad53..7dfd862ddc 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.3.0-SNAPSHOT + 5.2.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 60c235a9ec..4992f9d6bc 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.3.0-SNAPSHOT + 5.2.0-SNAPSHOT operator-framework-junit-5 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 253907eb1e..031f903ee0 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.3.0-SNAPSHOT + 5.2.0-SNAPSHOT operator-framework diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index 70485a2f3e..85658a13a6 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.3.0-SNAPSHOT + 5.2.0-SNAPSHOT sample-leader-election diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index a2334ca8c6..5ace3c48a8 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.3.0-SNAPSHOT + 5.2.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 6079d3bb71..4ce07ce912 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.3.0-SNAPSHOT + 5.2.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index c9fe8c2d06..15eab9585d 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.3.0-SNAPSHOT + 5.2.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index e25920b7da..7976d0fc7b 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.3.0-SNAPSHOT + 5.2.0-SNAPSHOT sample-webpage-operator From ba30d9bea0e564696ca3534977f95bd9a6cf750d Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Thu, 30 Oct 2025 08:22:09 -0400 Subject: [PATCH 12/26] Annotation removal using locking (#3015) Signed-off-by: Steve Hawkins --- .../operator/api/config/ConfigurationService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 6ed9b7ff64..90b8ebaf7b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -44,6 +44,8 @@ import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSIONS; + /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { From 906ac34fc8c580d65b8784c78c79380301b3bcee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sat, 1 Nov 2025 00:54:45 +0100 Subject: [PATCH 13/26] feat: comparable resource version utils --- .../io/javaoperatorsdk/operator/Operator.java | 2 +- ...tils.java => ReconcilerUtilsInternal.java} | 4 +- .../config/AbstractConfigurationService.java | 4 +- .../api/config/BaseConfigurationService.java | 4 +- .../api/config/ControllerConfiguration.java | 12 +- .../informer/InformerConfiguration.java | 4 +- .../api/reconciler/ReconcilerUtils.java | 124 ++++++++++++++++++ .../KubernetesDependentResource.java | 2 - .../event/ReconciliationDispatcher.java | 19 +-- .../controller/ControllerEventSource.java | 2 +- .../source/informer/InformerEventSource.java | 23 +--- .../source/informer/InformerManager.java | 4 +- .../source/informer/InformerWrapper.java | 6 +- .../informer/ManagedInformerEventSource.java | 16 +++ .../javaoperatorsdk/operator/OperatorIT.java | 2 +- ....java => ReconcilerUtilsInternalTest.java} | 28 ++-- .../GenericKubernetesResourceMatcherTest.java | 4 +- .../GenericResourceUpdaterTest.java | 4 +- ...dGenericKubernetesResourceMatcherTest.java | 4 +- .../controller/ControllerEventSourceTest.java | 4 +- .../junit/LocallyRunOperatorExtension.java | 10 +- .../baseapi/LeaderElectionPermissionIT.java | 6 +- .../BuiltInResourceCleanerIT.java | 2 +- .../InfrastructureClientIT.java | 12 +- .../baseapi/simple/TestReconciler.java | 4 +- .../config/BaseConfigurationServiceTest.java | 6 +- .../DefaultConfigurationServiceTest.java | 4 +- .../ExternalStateReconciler.java | 4 +- .../InformerRelatedBehaviorITS.java | 14 +- .../ServiceDependentResource.java | 2 +- .../ServiceDependentResource.java | 2 +- .../StandaloneDependentTestReconciler.java | 4 +- ...lSetDesiredSanitizerDependentResource.java | 4 +- .../dependent/BaseService.java | 4 +- .../dependent/BaseStatefulSet.java | 4 +- .../DeploymentDependentResource.java | 4 +- .../sample/DeploymentDependentResource.java | 4 +- .../sample/ServiceDependentResource.java | 5 +- .../operator/sample/Utils.java | 2 +- .../operator/sample/WebPageReconciler.java | 7 +- .../DeploymentDependentResource.java | 2 +- .../ServiceDependentResource.java | 2 +- 42 files changed, 251 insertions(+), 129 deletions(-) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{ReconcilerUtils.java => ReconcilerUtilsInternal.java} (99%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/{ReconcilerUtilsTest.java => ReconcilerUtilsInternalTest.java} (84%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java index 7aa29c98ae..b56676ac45 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java @@ -258,7 +258,7 @@ public

RegisteredController

register( "Cannot register reconciler with name " + reconciler.getClass().getCanonicalName() + " reconciler named " - + ReconcilerUtils.getNameFor(reconciler) + + ReconcilerUtilsInternal.getNameFor(reconciler) + " because its configuration cannot be found.\n" + " Known reconcilers are: " + configurationService.getKnownReconcilerNames()); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternal.java similarity index 99% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternal.java index 354c2aa420..1523b792a5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternal.java @@ -34,7 +34,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @SuppressWarnings("rawtypes") -public class ReconcilerUtils { +public class ReconcilerUtilsInternal { private static final String FINALIZER_NAME_SUFFIX = "/finalizer"; protected static final String MISSING_GROUP_SUFFIX = ".javaoperatorsdk.io"; @@ -46,7 +46,7 @@ public class ReconcilerUtils { Pattern.compile(".*http(s?)://[^/]*/api(s?)/(\\S*).*"); // NOSONAR: input is controlled // prevent instantiation of util class - private ReconcilerUtils() {} + private ReconcilerUtilsInternal() {} public static boolean isFinalizerValid(String finalizer) { return HasMetadata.validateFinalizer(finalizer); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index b85ee03fcb..a1b37d6fe9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -22,7 +22,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; /** @@ -145,7 +145,7 @@ private String getReconcilersNameMessage() { } protected String keyFor(Reconciler reconciler) { - return ReconcilerUtils.getNameFor(reconciler); + return ReconcilerUtilsInternal.getNameFor(reconciler); } @SuppressWarnings("unused") diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java index 18ef8b598c..ca14043750 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java @@ -28,7 +28,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.Utils.Configurator; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; @@ -265,7 +265,7 @@ private

ResolvedControllerConfiguration

controllerCon io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration annotation) { final var resourceClass = getResourceClassResolver().getPrimaryResourceClass(reconcilerClass); - final var name = ReconcilerUtils.getNameFor(reconcilerClass); + final var name = ReconcilerUtilsInternal.getNameFor(reconcilerClass); final var generationAware = valueOrDefaultFromAnnotation( annotation, diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index a601092454..e0834621fc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -20,7 +20,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec; import io.javaoperatorsdk.operator.api.reconciler.MaxReconciliationInterval; @@ -42,16 +42,18 @@ default String getName() { } default String getFinalizerName() { - return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); + return ReconcilerUtilsInternal.getDefaultFinalizerName(getResourceClass()); } static String ensureValidName(String name, String reconcilerClassName) { - return name != null ? name : ReconcilerUtils.getDefaultReconcilerName(reconcilerClassName); + return name != null + ? name + : ReconcilerUtilsInternal.getDefaultReconcilerName(reconcilerClassName); } static String ensureValidFinalizerName(String finalizer, String resourceTypeName) { if (finalizer != null && !finalizer.isBlank()) { - if (ReconcilerUtils.isFinalizerValid(finalizer)) { + if (ReconcilerUtilsInternal.isFinalizerValid(finalizer)) { return finalizer; } else { throw new IllegalArgumentException( @@ -61,7 +63,7 @@ static String ensureValidFinalizerName(String finalizer, String resourceTypeName + " for details"); } } else { - return ReconcilerUtils.getDefaultFinalizerName(resourceTypeName); + return ReconcilerUtilsInternal.getDefaultFinalizerName(resourceTypeName); } } 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 30a1a32e8a..f6caa4fe4d 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 @@ -25,7 +25,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.informers.cache.ItemStore; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.reconciler.Constants; @@ -92,7 +92,7 @@ private InformerConfiguration(Class resourceClass) { // controller // where GenericKubernetesResource now does not apply ? GenericKubernetesResource.class.getSimpleName() - : ReconcilerUtils.getResourceTypeName(resourceClass); + : ReconcilerUtilsInternal.getResourceTypeName(resourceClass); } @SuppressWarnings({"rawtypes", "unchecked"}) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java new file mode 100644 index 0000000000..48a1bb46b9 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java @@ -0,0 +1,124 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.util.function.UnaryOperator; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; + +public class ReconcilerUtils { + // toto namespace handling + // todo compare resource version if multiple event sources provide the same resource + // for json patch make sense to retry (json merge patch?) + + public static R ssa(Context context, R resource) { + return handleResourcePatch( + context, + resource, + r -> + context + .getClient() + .resource(r) + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build())); + } + + public static R ssaStatus( + Context context, R resource) { + return handleResourcePatch( + context, + resource, + r -> + context + .getClient() + .resource(r) + .subresource("status") + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build())); + } + + public static

P ssaPrimary(Context

context, P resource) { + return handleResourcePatch( + resource, + r -> + context + .getClient() + .resource(r) + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()), + true, + context.eventSourceRetriever().getControllerEventSource()); + } + + public static

P ssaStatusPrimary(Context

context, P resource) { + return handleResourcePatch( + resource, + r -> + context + .getClient() + .resource(r) + .subresource("status") + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()), + true, + context.eventSourceRetriever().getControllerEventSource()); + } + + public static R handleResourcePatch( + Context context, R resource, UnaryOperator updateOperation) { + var esList = context.eventSourceRetriever().getEventSourcesFor(resource.getClass()); + if (esList.isEmpty()) { + throw new IllegalStateException("No event source found for type: " + resource.getClass()); + } + if (esList.size() > 1) { + throw new IllegalStateException( + "Multiple event sources found for: " + + resource.getClass() + + " please provide the target event source"); + } + var es = esList.get(0); + if (es instanceof ManagedInformerEventSource mes) { + return handleResourcePatch(resource, updateOperation, true, mes); + } else { + throw new IllegalStateException( + "Target event source must be a subclass off " + + ManagedInformerEventSource.class.getName()); + } + } + + @SuppressWarnings("unchecked") + private static R handleResourcePatch( + R resource, + UnaryOperator updateOperation, + boolean doNotLock, + ManagedInformerEventSource ies) { + var resourceVersion = resource.getMetadata().getResourceVersion(); + try { + if (resourceVersion != null && doNotLock) { + resource.getMetadata().setResourceVersion(null); + } + return (R) ies.updateAndCacheResource(resource, updateOperation); + } finally { + if (resourceVersion != null && doNotLock) { + resource.getMetadata().setResourceVersion(resourceVersion); + } + } + } +} 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 5d53b807cc..846e3f5ceb 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 @@ -77,7 +77,6 @@ protected R handleCreate(R desired, P primary, Context

context) { .orElseThrow() .updateAndCacheResource( desired, - context, toCreate -> KubernetesDependentResource.super.handleCreate(toCreate, primary, context)); } @@ -87,7 +86,6 @@ protected R handleUpdate(R actual, R desired, P primary, Context

context) { .orElseThrow() .updateAndCacheResource( desired, - context, toUpdate -> KubernetesDependentResource.super.handleUpdate(actual, toUpdate, primary, context)); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 8a1ddb4c5a..9f5c8d93d4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -33,13 +33,14 @@ import io.fabric8.kubernetes.client.dsl.base.PatchContext; import io.fabric8.kubernetes.client.dsl.base.PatchType; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.BaseControl; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.Controller; @@ -203,7 +204,7 @@ private PostExecutionControl

reconcileExecution( } if (updateControl.isPatchStatus()) { - customResourceFacade.patchStatus(toUpdate, originalResource); + customResourceFacade.patchStatus(context, toUpdate, originalResource); } return createPostExecutionControl(updatedCustomResource, updateControl, executionScope); } @@ -241,7 +242,7 @@ public boolean isLastAttempt() { try { updatedResource = customResourceFacade.patchStatus( - errorStatusUpdateControl.getResource().orElseThrow(), originalResource); + context, errorStatusUpdateControl.getResource().orElseThrow(), originalResource); } catch (Exception ex) { int code = ex instanceof KubernetesClientException kcex ? kcex.getCode() : -1; Level exceptionLevel = Level.ERROR; @@ -529,20 +530,14 @@ public R patchResource(R resource, R originalResource) { } } - public R patchStatus(R resource, R originalResource) { + public R patchStatus(Context context, R resource, R originalResource) { log.trace("Patching status for resource: {} with ssa: {}", resource, useSSA); if (useSSA) { var managedFields = resource.getMetadata().getManagedFields(); try { resource.getMetadata().setManagedFields(null); var res = resource(resource); - return res.subresource("status") - .patch( - new PatchContext.Builder() - .withFieldManager(fieldManager) - .withForce(true) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build()); + return ReconcilerUtils.ssaStatusPrimary(context, resource); } finally { resource.getMetadata().setManagedFields(managedFields); } @@ -562,7 +557,7 @@ private R editStatus(R resource, R originalResource) { var res = resource(clonedOriginal); return res.editStatus( r -> { - ReconcilerUtils.setStatus(r, ReconcilerUtils.getStatus(resource)); + ReconcilerUtilsInternal.setStatus(r, ReconcilerUtilsInternal.getStatus(resource)); return r; }); } finally { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java index f7ed9fdc8e..3c232ceb07 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java @@ -32,7 +32,7 @@ import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; -import static io.javaoperatorsdk.operator.ReconcilerUtils.handleKubernetesClientException; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.handleKubernetesClientException; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; import static io.javaoperatorsdk.operator.processing.event.source.controller.InternalEventFilters.*; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 0feb3dc2a8..fa0dc9502d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -17,7 +17,6 @@ import java.util.Optional; import java.util.Set; -import java.util.function.UnaryOperator; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -98,22 +97,6 @@ private InformerEventSource( genericFilter = informerConfig.getGenericFilter(); } - public R updateAndCacheResource( - R resourceToUpdate, Context context, UnaryOperator updateMethod) { - ResourceID id = ResourceID.fromResource(resourceToUpdate); - if (log.isDebugEnabled()) { - log.debug("Update and cache: {}", id); - } - try { - temporaryResourceCache.startModifying(id); - var updated = updateMethod.apply(resourceToUpdate); - handleRecentResourceUpdate(id, updated, resourceToUpdate); - return updated; - } finally { - temporaryResourceCache.doneModifying(id); - } - } - @Override public void onAdd(R newResource) { if (log.isDebugEnabled()) { @@ -233,15 +216,15 @@ public Set getSecondaryResources(P primary) { @Override public void handleRecentResourceUpdate( ResourceID resourceID, R resource, R previousVersionOfResource) { - handleRecentCreateOrUpdate(Operation.UPDATE, resource, previousVersionOfResource); + handleRecentCreateOrUpdate(resource); } @Override public void handleRecentResourceCreate(ResourceID resourceID, R resource) { - handleRecentCreateOrUpdate(Operation.ADD, resource, null); + handleRecentCreateOrUpdate(resource); } - private void handleRecentCreateOrUpdate(Operation operation, R newResource, R oldResource) { + private void handleRecentCreateOrUpdate(R newResource) { primaryToSecondaryIndex.onAddOrUpdate(newResource); temporaryResourceCache.putResource(newResource); } 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 abd2b6a752..42e06c9d9a 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 @@ -32,7 +32,7 @@ import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Informable; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; @@ -253,7 +253,7 @@ public String toString() { final var informerConfig = configuration.getInformerConfig(); final var selector = informerConfig.getLabelSelector(); return "InformerManager [" - + ReconcilerUtils.getResourceTypeNameWithVersion(configuration.getResourceClass()) + + ReconcilerUtilsInternal.getResourceTypeNameWithVersion(configuration.getResourceClass()) + "] watching: " + informerConfig.getEffectiveNamespaces(controllerConfiguration) + (selector != null ? " selector: " + selector : ""); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index c3a4a9f2c1..60497bc0c9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -35,7 +35,7 @@ import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.informers.cache.Cache; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.health.InformerHealthIndicator; import io.javaoperatorsdk.operator.health.Status; @@ -131,7 +131,7 @@ public void start() throws OperatorException { } } catch (Exception e) { - ReconcilerUtils.handleKubernetesClientException( + ReconcilerUtilsInternal.handleKubernetesClientException( e, HasMetadata.getFullResourceName(informer.getApiTypeClass())); throw new OperatorException( "Couldn't start informer for " + versionedFullResourceName() + " resources", e); @@ -143,7 +143,7 @@ private String versionedFullResourceName() { if (apiTypeClass.isAssignableFrom(GenericKubernetesResource.class)) { return GenericKubernetesResource.class.getSimpleName(); } - return ReconcilerUtils.getResourceTypeNameWithVersion(apiTypeClass); + return ReconcilerUtilsInternal.getResourceTypeNameWithVersion(apiTypeClass); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index af30617d92..230c649872 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -22,6 +22,7 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.slf4j.Logger; @@ -97,6 +98,21 @@ public void changeNamespaces(Set namespaces) { } } + public R updateAndCacheResource(R resourceToUpdate, UnaryOperator updateMethod) { + ResourceID id = ResourceID.fromResource(resourceToUpdate); + if (log.isDebugEnabled()) { + log.debug("Update and cache: {}", id); + } + try { + temporaryResourceCache.startModifying(id); + var updated = updateMethod.apply(resourceToUpdate); + handleRecentResourceUpdate(id, updated, resourceToUpdate); + return updated; + } finally { + temporaryResourceCache.doneModifying(id); + } + } + @SuppressWarnings("unchecked") @Override public synchronized void start() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java index c87c986f99..e5dae6ca80 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java @@ -45,7 +45,7 @@ void shouldBePossibleToRetrieveNumberOfRegisteredControllers() { void shouldBePossibleToRetrieveRegisteredControllerByName() { final var operator = new Operator(); final var reconciler = new FooReconciler(); - final var name = ReconcilerUtils.getNameFor(reconciler); + final var name = ReconcilerUtilsInternal.getNameFor(reconciler); var registeredControllers = operator.getRegisteredControllers(); assertTrue(operator.getRegisteredController(name).isEmpty()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternalTest.java similarity index 84% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternalTest.java index 3bbe2a894b..12e45b9c23 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternalTest.java @@ -32,17 +32,17 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultFinalizerName; -import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultNameFor; -import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultReconcilerName; -import static io.javaoperatorsdk.operator.ReconcilerUtils.handleKubernetesClientException; -import static io.javaoperatorsdk.operator.ReconcilerUtils.isFinalizerValid; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.getDefaultFinalizerName; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.getDefaultNameFor; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.getDefaultReconcilerName; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.handleKubernetesClientException; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.isFinalizerValid; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -class ReconcilerUtilsTest { +class ReconcilerUtilsInternalTest { public static final String RESOURCE_URI = "https://kubernetes.docker.internal:6443/apis/tomcatoperator.io/v1/tomcats"; @@ -71,7 +71,7 @@ void equalsSpecObject() { var d1 = createTestDeployment(); var d2 = createTestDeployment(); - assertThat(ReconcilerUtils.specsEqual(d1, d2)).isTrue(); + assertThat(ReconcilerUtilsInternal.specsEqual(d1, d2)).isTrue(); } @Test @@ -80,7 +80,7 @@ void equalArbitraryDifferentSpecsOfObjects() { var d2 = createTestDeployment(); d2.getSpec().getTemplate().getSpec().setHostname("otherhost"); - assertThat(ReconcilerUtils.specsEqual(d1, d2)).isFalse(); + assertThat(ReconcilerUtilsInternal.specsEqual(d1, d2)).isFalse(); } @Test @@ -89,7 +89,7 @@ void getsSpecWithReflection() { deployment.setSpec(new DeploymentSpec()); deployment.getSpec().setReplicas(5); - DeploymentSpec spec = (DeploymentSpec) ReconcilerUtils.getSpec(deployment); + DeploymentSpec spec = (DeploymentSpec) ReconcilerUtilsInternal.getSpec(deployment); assertThat(spec.getReplicas()).isEqualTo(5); } @@ -97,10 +97,10 @@ void getsSpecWithReflection() { void properlyHandlesNullSpec() { Namespace ns = new Namespace(); - final var spec = ReconcilerUtils.getSpec(ns); + final var spec = ReconcilerUtilsInternal.getSpec(ns); assertThat(spec).isNull(); - ReconcilerUtils.setSpec(ns, null); + ReconcilerUtilsInternal.setSpec(ns, null); } @Test @@ -111,7 +111,7 @@ void setsSpecWithReflection() { DeploymentSpec newSpec = new DeploymentSpec(); newSpec.setReplicas(1); - ReconcilerUtils.setSpec(deployment, newSpec); + ReconcilerUtilsInternal.setSpec(deployment, newSpec); assertThat(deployment.getSpec().getReplicas()).isEqualTo(1); } @@ -124,7 +124,7 @@ void setsSpecCustomResourceWithReflection() { TomcatSpec newSpec = new TomcatSpec(); newSpec.setReplicas(1); - ReconcilerUtils.setSpec(tomcat, newSpec); + ReconcilerUtilsInternal.setSpec(tomcat, newSpec); assertThat(tomcat.getSpec().getReplicas()).isEqualTo(1); } @@ -132,7 +132,7 @@ void setsSpecCustomResourceWithReflection() { @Test void loadYamlAsBuilder() { DeploymentBuilder builder = - ReconcilerUtils.loadYaml(DeploymentBuilder.class, getClass(), "deployment.yaml"); + ReconcilerUtilsInternal.loadYaml(DeploymentBuilder.class, getClass(), "deployment.yaml"); builder.accept(ContainerBuilder.class, c -> c.withImage("my-image")); Deployment deployment = builder.editMetadata().withName("my-deployment").and().build(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 8a920b28b9..8dd7283fb9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -26,7 +26,7 @@ import io.fabric8.kubernetes.api.model.apps.DeploymentStatusBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.MockKubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; @@ -198,7 +198,7 @@ ConfigMap createConfigMap() { } Deployment createDeployment() { - return ReconcilerUtils.loadYaml( + return ReconcilerUtilsInternal.loadYaml( Deployment.class, GenericKubernetesResourceMatcherTest.class, "nginx-deployment.yaml"); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterTest.java index 3b6580c5d3..70d664f652 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterTest.java @@ -25,7 +25,7 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.MockKubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -131,7 +131,7 @@ void checkServiceAccount() { } Deployment createDeployment() { - return ReconcilerUtils.loadYaml( + return ReconcilerUtilsInternal.loadYaml( Deployment.class, GenericResourceUpdaterTest.class, "nginx-deployment.yaml"); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java index bbcfa704b5..c4d2f2c77d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java @@ -32,7 +32,7 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -419,7 +419,7 @@ void testSortListItems() { } private static R loadResource(String fileName, Class clazz) { - return ReconcilerUtils.loadYaml( + return ReconcilerUtilsInternal.loadYaml( clazz, SSABasedGenericKubernetesResourceMatcherTest.class, fileName); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java index dcd10b4225..14a4526698 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java @@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.MockKubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; @@ -46,7 +46,7 @@ class ControllerEventSourceTest extends AbstractEventSourceTestBase, EventHandler> { public static final String FINALIZER = - ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class); + ReconcilerUtilsInternal.getDefaultFinalizerName(TestCustomResource.class); private final TestController testController = new TestController(true); private final ControllerConfiguration controllerConfig = mock(ControllerConfiguration.class); diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java index 0a33293b53..1faa545f54 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java @@ -44,7 +44,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.LocalPortForward; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.RegisteredController; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; @@ -140,7 +140,7 @@ public static Builder builder() { } public static void applyCrd(Class resourceClass, KubernetesClient client) { - applyCrd(ReconcilerUtils.getResourceTypeName(resourceClass), client); + applyCrd(ReconcilerUtilsInternal.getResourceTypeName(resourceClass), client); } /** @@ -192,7 +192,7 @@ private static void applyCrd(String crdString, String path, KubernetesClient cli * @param crClass the custom resource class for which we want to apply the CRD */ public void applyCrd(Class crClass) { - applyCrd(ReconcilerUtils.getResourceTypeName(crClass)); + applyCrd(ReconcilerUtilsInternal.getResourceTypeName(crClass)); } /** @@ -203,7 +203,7 @@ public void applyCrd(Class crClass) { * * @param resourceTypeName the resource type name associated with the CRD to be applied, * typically, given a resource type, its name would be obtained using {@link - * ReconcilerUtils#getResourceTypeName(Class)} + * ReconcilerUtilsInternal#getResourceTypeName(Class)} */ public void applyCrd(String resourceTypeName) { // first attempt to use a manually defined CRD @@ -296,7 +296,7 @@ protected void before(ExtensionContext context) { ref.controllerConfigurationOverrider.accept(oconfig); } - final var resourceTypeName = ReconcilerUtils.getResourceTypeName(resourceClass); + final var resourceTypeName = ReconcilerUtilsInternal.getResourceTypeName(resourceClass); // only try to apply a CRD for the reconciler if it is associated to a CR if (CustomResource.class.isAssignableFrom(resourceClass)) { applyCrd(resourceTypeName); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java index db99324ae2..457de54ca3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java @@ -26,7 +26,7 @@ import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -87,14 +87,14 @@ public UpdateControl reconcile(ConfigMap resource, Context private void applyRoleBinding() { var clusterRoleBinding = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( RoleBinding.class, this.getClass(), "leader-elector-stop-noaccess-role-binding.yaml"); adminClient.resource(clusterRoleBinding).createOrReplace(); } private void applyRole() { var role = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( Role.class, this.getClass(), "leader-elector-stop-role-noaccess.yaml"); adminClient.resource(role).createOrReplace(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java index 9667c22486..812007708a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java @@ -85,7 +85,7 @@ void cleanerIsCalledOnBuiltInResource() { Service testService() { Service service = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( Service.class, StandaloneDependentResourceIT.class, "/io/javaoperatorsdk/operator/service-template.yaml"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientIT.java index 59faaae90b..eb39fa0657 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientIT.java @@ -28,7 +28,7 @@ import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.KubernetesClientException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -127,23 +127,25 @@ void shouldNotAccessNotPermittedResources() { private void applyClusterRoleBinding(String filename) { var clusterRoleBinding = - ReconcilerUtils.loadYaml(ClusterRoleBinding.class, this.getClass(), filename); + ReconcilerUtilsInternal.loadYaml(ClusterRoleBinding.class, this.getClass(), filename); operator.getInfrastructureKubernetesClient().resource(clusterRoleBinding).serverSideApply(); } private void applyClusterRole(String filename) { - var clusterRole = ReconcilerUtils.loadYaml(ClusterRole.class, this.getClass(), filename); + var clusterRole = + ReconcilerUtilsInternal.loadYaml(ClusterRole.class, this.getClass(), filename); operator.getInfrastructureKubernetesClient().resource(clusterRole).serverSideApply(); } private void removeClusterRoleBinding(String filename) { var clusterRoleBinding = - ReconcilerUtils.loadYaml(ClusterRoleBinding.class, this.getClass(), filename); + ReconcilerUtilsInternal.loadYaml(ClusterRoleBinding.class, this.getClass(), filename); operator.getInfrastructureKubernetesClient().resource(clusterRoleBinding).delete(); } private void removeClusterRole(String filename) { - var clusterRole = ReconcilerUtils.loadYaml(ClusterRole.class, this.getClass(), filename); + var clusterRole = + ReconcilerUtilsInternal.loadYaml(ClusterRole.class, this.getClass(), filename); operator.getInfrastructureKubernetesClient().resource(clusterRole).delete(); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/TestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/TestReconciler.java index b614b97f3a..6bb184d374 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/TestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/TestReconciler.java @@ -25,7 +25,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @@ -38,7 +38,7 @@ public class TestReconciler private static final Logger log = LoggerFactory.getLogger(TestReconciler.class); public static final String FINALIZER_NAME = - ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class); + ReconcilerUtilsInternal.getDefaultFinalizerName(TestCustomResource.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); private final AtomicInteger numberOfCleanupExecutions = new AtomicInteger(0); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java index 370f09509f..ffd0f6b904 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java @@ -29,7 +29,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Service; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.AnnotationConfigurable; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.dependent.ConfigurationConverter; @@ -133,13 +133,13 @@ void missingAnnotationCreatesDefaultConfig() { final var reconciler = new MissingAnnotationReconciler(); var config = configFor(reconciler); - assertThat(config.getName()).isEqualTo(ReconcilerUtils.getNameFor(reconciler)); + assertThat(config.getName()).isEqualTo(ReconcilerUtilsInternal.getNameFor(reconciler)); assertThat(config.getRetry()).isInstanceOf(GenericRetry.class); assertThat(config.getRateLimiter()).isInstanceOf(LinearRateLimiter.class); assertThat(config.maxReconciliationInterval()).hasValue(Duration.ofHours(DEFAULT_INTERVAL)); assertThat(config.fieldManager()).isEqualTo(config.getName()); assertThat(config.getFinalizerName()) - .isEqualTo(ReconcilerUtils.getDefaultFinalizerName(config.getResourceClass())); + .isEqualTo(ReconcilerUtilsInternal.getDefaultFinalizerName(config.getResourceClass())); final var informerConfig = config.getInformerConfig(); assertThat(informerConfig.getLabelSelector()).isNull(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index 1b328ccaf9..fa31575b9e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -20,7 +20,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.Version; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -40,7 +40,7 @@ void returnsValuesFromControllerAnnotationFinalizer() { assertEquals( CustomResource.getCRDName(TestCustomResource.class), configuration.getResourceTypeName()); assertEquals( - ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class), + ReconcilerUtilsInternal.getDefaultFinalizerName(TestCustomResource.class), configuration.getFinalizerName()); assertEquals(TestCustomResource.class, configuration.getResourceClass()); assertFalse(configuration.isGenerationAware()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java index 89d1dee94b..cc574fcbe1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java @@ -110,9 +110,7 @@ private void createExternalResource( // This is critical in this case, since on next reconciliation if it would not be in the cache // it would be created again. configMapEventSource.updateAndCacheResource( - configMap, - context, - toCreate -> context.getClient().configMaps().resource(toCreate).create()); + configMap, toCreate -> context.getClient().configMaps().resource(toCreate).create()); externalResourceEventSource.handleRecentResourceCreate(primaryID, createdResource); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java index 221d7363a3..ce98af58e0 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java @@ -34,7 +34,7 @@ import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.health.InformerHealthIndicator; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; @@ -399,23 +399,25 @@ private void setFullResourcesAccess() { private void addRoleBindingsToTestNamespaces() { var role = - ReconcilerUtils.loadYaml(Role.class, this.getClass(), "rbac-test-only-main-ns-access.yaml"); + ReconcilerUtilsInternal.loadYaml( + Role.class, this.getClass(), "rbac-test-only-main-ns-access.yaml"); adminClient.resource(role).inNamespace(actualNamespace).createOrReplace(); var roleBinding = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( RoleBinding.class, this.getClass(), "rbac-test-only-main-ns-access-binding.yaml"); adminClient.resource(roleBinding).inNamespace(actualNamespace).createOrReplace(); } private void applyClusterRoleBinding() { var clusterRoleBinding = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( ClusterRoleBinding.class, this.getClass(), "rbac-test-role-binding.yaml"); adminClient.resource(clusterRoleBinding).createOrReplace(); } private void applyClusterRole(String filename) { - var clusterRole = ReconcilerUtils.loadYaml(ClusterRole.class, this.getClass(), filename); + var clusterRole = + ReconcilerUtilsInternal.loadYaml(ClusterRole.class, this.getClass(), filename); adminClient.resource(clusterRole).createOrReplace(); } @@ -431,7 +433,7 @@ private Namespace namespace(String name) { private void removeClusterRoleBinding() { var clusterRoleBinding = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( ClusterRoleBinding.class, this.getClass(), "rbac-test-role-binding.yaml"); adminClient.resource(clusterRoleBinding).delete(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java index 1bb34de16c..fb243251f3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java @@ -26,7 +26,7 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesResourceMatcher; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.loadYaml; @KubernetesDependent public class ServiceDependentResource diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java index 7cd65bd7ef..6a998b3ea4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java @@ -25,7 +25,7 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesResourceMatcher; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.loadYaml; @KubernetesDependent public class ServiceDependentResource diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java index 6f97be1be7..92f033d681 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java @@ -20,7 +20,7 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClientException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; @@ -90,7 +90,7 @@ protected Deployment desired( StandaloneDependentTestCustomResource primary, Context context) { Deployment deployment = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( Deployment.class, StandaloneDependentResourceIT.class, "/io/javaoperatorsdk/operator/nginx-deployment.yaml"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java index e86c772cda..e4bcaac460 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java @@ -17,7 +17,7 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.apps.StatefulSet; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; @@ -32,7 +32,7 @@ protected StatefulSet desired( StatefulSetDesiredSanitizerCustomResource primary, Context context) { var template = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( StatefulSet.class, getClass(), "/io/javaoperatorsdk/operator/statefulset.yaml"); template.setMetadata( new ObjectMetaBuilder() diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseService.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseService.java index 06abcc0889..7a0d50debf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseService.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseService.java @@ -19,7 +19,7 @@ import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.workflow.complexdependent.ComplexWorkflowCustomResource; @@ -33,7 +33,7 @@ public BaseService(String component) { protected Service desired( ComplexWorkflowCustomResource primary, Context context) { var template = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( Service.class, getClass(), "/io/javaoperatorsdk/operator/workflow/complexdependent/service.yaml"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseStatefulSet.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseStatefulSet.java index b0a7b60805..1e4aa73e80 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseStatefulSet.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseStatefulSet.java @@ -19,7 +19,7 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.workflow.complexdependent.ComplexWorkflowCustomResource; @@ -32,7 +32,7 @@ public BaseStatefulSet(String component) { protected StatefulSet desired( ComplexWorkflowCustomResource primary, Context context) { var template = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( StatefulSet.class, getClass(), "/io/javaoperatorsdk/operator/workflow/complexdependent/statefulset.yaml"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java index b9aa595b76..e5c7f726f5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java @@ -16,7 +16,7 @@ package io.javaoperatorsdk.operator.workflow.workflowallfeature; import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDNoGCKubernetesDependentResource; @@ -27,7 +27,7 @@ public class DeploymentDependentResource protected Deployment desired( WorkflowAllFeatureCustomResource primary, Context context) { Deployment deployment = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( Deployment.class, WorkflowAllFeatureIT.class, "/io/javaoperatorsdk/operator/nginx-deployment.yaml"); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 0347b726ac..c4a47069e2 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -18,7 +18,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.informer.Informer; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; @@ -36,7 +36,7 @@ private static String tomcatImage(Tomcat tomcat) { @Override protected Deployment desired(Tomcat tomcat, Context context) { Deployment deployment = - ReconcilerUtils.loadYaml(Deployment.class, getClass(), "deployment.yaml"); + ReconcilerUtilsInternal.loadYaml(Deployment.class, getClass(), "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); final String tomcatName = tomcatMetadata.getName(); deployment = diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index 72f430528e..bcb0e80026 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -18,7 +18,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.informer.Informer; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; @@ -31,7 +31,8 @@ public class ServiceDependentResource extends CRUDKubernetesDependentResource context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); - return new ServiceBuilder(ReconcilerUtils.loadYaml(Service.class, getClass(), "service.yaml")) + return new ServiceBuilder( + ReconcilerUtilsInternal.loadYaml(Service.class, getClass(), "service.yaml")) .editMetadata() .withName(tomcatMetadata.getName()) .withNamespace(tomcatMetadata.getNamespace()) diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java index ab4ed8a337..ecfe66d329 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java @@ -21,7 +21,7 @@ import io.javaoperatorsdk.operator.sample.customresource.WebPage; import io.javaoperatorsdk.operator.sample.customresource.WebPageStatus; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.loadYaml; public class Utils { diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 94b460474f..941a159542 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -27,7 +27,7 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -219,7 +219,8 @@ private boolean match(ConfigMap desiredHtmlConfigMap, ConfigMap existingConfigMa } private Service makeDesiredService(WebPage webPage, String ns, Deployment desiredDeployment) { - Service desiredService = ReconcilerUtils.loadYaml(Service.class, getClass(), "service.yaml"); + Service desiredService = + ReconcilerUtilsInternal.loadYaml(Service.class, getClass(), "service.yaml"); desiredService.getMetadata().setName(serviceName(webPage)); desiredService.getMetadata().setNamespace(ns); desiredService.getMetadata().setLabels(lowLevelLabel()); @@ -233,7 +234,7 @@ private Service makeDesiredService(WebPage webPage, String ns, Deployment desire private Deployment makeDesiredDeployment( WebPage webPage, String deploymentName, String ns, String configMapName) { Deployment desiredDeployment = - ReconcilerUtils.loadYaml(Deployment.class, getClass(), "deployment.yaml"); + ReconcilerUtilsInternal.loadYaml(Deployment.class, getClass(), "deployment.yaml"); desiredDeployment.getMetadata().setName(deploymentName); desiredDeployment.getMetadata().setNamespace(ns); desiredDeployment.getMetadata().setLabels(lowLevelLabel()); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java index 6d1f7cc911..e383633ab1 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java @@ -27,7 +27,7 @@ import io.javaoperatorsdk.operator.sample.Utils; import io.javaoperatorsdk.operator.sample.customresource.WebPage; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.loadYaml; import static io.javaoperatorsdk.operator.sample.Utils.configMapName; import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; import static io.javaoperatorsdk.operator.sample.WebPageManagedDependentsReconciler.SELECTOR; diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java index 3dbc784887..02204d415a 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java @@ -25,7 +25,7 @@ import io.javaoperatorsdk.operator.sample.Utils; import io.javaoperatorsdk.operator.sample.customresource.WebPage; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.loadYaml; import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; import static io.javaoperatorsdk.operator.sample.Utils.serviceName; import static io.javaoperatorsdk.operator.sample.WebPageManagedDependentsReconciler.SELECTOR; From 34de531f0a8b697c9a43a31f4ef9b9308e1c9804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 16 Nov 2025 19:20:43 +0100 Subject: [PATCH 14/26] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/reconciler/ReconcileUtils.java | 179 ++++++++++++++++++ .../api/reconciler/ReconcilerUtils.java | 124 ------------ .../event/ReconciliationDispatcher.java | 22 +-- .../event/ReconciliationDispatcherTest.java | 45 ++--- 4 files changed, 213 insertions(+), 157 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java new file mode 100644 index 0000000000..6cd6a4db69 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java @@ -0,0 +1,179 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.util.function.UnaryOperator; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; + +public class ReconcileUtils { + + private ReconcileUtils() {} + + // todo javadoc + // todo move finalizers mtehods & deprecate + // todo namespace handling + // todo compare resource version if multiple event sources provide the same resource + // for json patch make sense to retry for ? + + public static R serverSideApply( + Context context, R resource) { + return resourcePatch( + context, + resource, + r -> + context + .getClient() + .resource(r) + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build())); + } + + public static R serverSideApplyStatus( + Context context, R resource) { + return resourcePatch( + context, + resource, + r -> + context + .getClient() + .resource(r) + .subresource("status") + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build())); + } + + public static

P serverSideApplyPrimary(Context

context, P resource) { + return resourcePatch( + resource, + r -> + context + .getClient() + .resource(r) + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()), + context.eventSourceRetriever().getControllerEventSource()); + } + + public static

P serverSideApplyPrimaryStatus( + Context

context, P resource) { + return resourcePatch( + resource, + r -> + context + .getClient() + .resource(r) + .subresource("status") + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()), + context.eventSourceRetriever().getControllerEventSource()); + } + + public static R update( + Context context, R resource) { + return resourcePatch(context, resource, r -> context.getClient().resource(r).update()); + } + + public static R updateStatus( + Context context, R resource) { + return resourcePatch(context, resource, r -> context.getClient().resource(r).updateStatus()); + } + + public static R jsonPatch( + Context context, R resource, UnaryOperator unaryOperator) { + return resourcePatch( + context, resource, r -> context.getClient().resource(r).edit(unaryOperator)); + } + + public static R jsonPatchStatus( + Context context, R resource, UnaryOperator unaryOperator) { + return resourcePatch( + context, resource, r -> context.getClient().resource(r).editStatus(unaryOperator)); + } + + public static R jsonPatchPrimary( + Context context, R resource, UnaryOperator unaryOperator) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).edit(unaryOperator), + context.eventSourceRetriever().getControllerEventSource()); + } + + public static R jsonPatchPrimaryStatus( + Context context, R resource, UnaryOperator unaryOperator) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).editStatus(unaryOperator), + context.eventSourceRetriever().getControllerEventSource()); + } + + public static R jsonMergePatch( + Context context, R resource) { + return resourcePatch(context, resource, r -> context.getClient().resource(r).patch()); + } + + public static R jsonMergePatchStatus( + Context context, R resource) { + return resourcePatch(context, resource, r -> context.getClient().resource(r).patchStatus()); + } + + public static R resourcePatch( + Context context, R resource, UnaryOperator updateOperation) { + var esList = context.eventSourceRetriever().getEventSourcesFor(resource.getClass()); + if (esList.isEmpty()) { + throw new IllegalStateException("No event source found for type: " + resource.getClass()); + } + if (esList.size() > 1) { + throw new IllegalStateException( + "Multiple event sources found for: " + + resource.getClass() + + " please provide the target event source"); + } + var es = esList.get(0); + if (es instanceof ManagedInformerEventSource mes) { + return resourcePatch(resource, updateOperation, mes); + } else { + throw new IllegalStateException( + "Target event source must be a subclass off " + + ManagedInformerEventSource.class.getName()); + } + } + + @SuppressWarnings("unchecked") + public static R resourcePatch( + R resource, UnaryOperator updateOperation, ManagedInformerEventSource ies) { + return (R) ies.updateAndCacheResource(resource, updateOperation); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java deleted file mode 100644 index 48a1bb46b9..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java +++ /dev/null @@ -1,124 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler; - -import java.util.function.UnaryOperator; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.dsl.base.PatchContext; -import io.fabric8.kubernetes.client.dsl.base.PatchType; -import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; - -public class ReconcilerUtils { - // toto namespace handling - // todo compare resource version if multiple event sources provide the same resource - // for json patch make sense to retry (json merge patch?) - - public static R ssa(Context context, R resource) { - return handleResourcePatch( - context, - resource, - r -> - context - .getClient() - .resource(r) - .patch( - new PatchContext.Builder() - .withForce(true) - .withFieldManager(context.getControllerConfiguration().fieldManager()) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build())); - } - - public static R ssaStatus( - Context context, R resource) { - return handleResourcePatch( - context, - resource, - r -> - context - .getClient() - .resource(r) - .subresource("status") - .patch( - new PatchContext.Builder() - .withForce(true) - .withFieldManager(context.getControllerConfiguration().fieldManager()) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build())); - } - - public static

P ssaPrimary(Context

context, P resource) { - return handleResourcePatch( - resource, - r -> - context - .getClient() - .resource(r) - .patch( - new PatchContext.Builder() - .withForce(true) - .withFieldManager(context.getControllerConfiguration().fieldManager()) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build()), - true, - context.eventSourceRetriever().getControllerEventSource()); - } - - public static

P ssaStatusPrimary(Context

context, P resource) { - return handleResourcePatch( - resource, - r -> - context - .getClient() - .resource(r) - .subresource("status") - .patch( - new PatchContext.Builder() - .withForce(true) - .withFieldManager(context.getControllerConfiguration().fieldManager()) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build()), - true, - context.eventSourceRetriever().getControllerEventSource()); - } - - public static R handleResourcePatch( - Context context, R resource, UnaryOperator updateOperation) { - var esList = context.eventSourceRetriever().getEventSourcesFor(resource.getClass()); - if (esList.isEmpty()) { - throw new IllegalStateException("No event source found for type: " + resource.getClass()); - } - if (esList.size() > 1) { - throw new IllegalStateException( - "Multiple event sources found for: " - + resource.getClass() - + " please provide the target event source"); - } - var es = esList.get(0); - if (es instanceof ManagedInformerEventSource mes) { - return handleResourcePatch(resource, updateOperation, true, mes); - } else { - throw new IllegalStateException( - "Target event source must be a subclass off " - + ManagedInformerEventSource.class.getName()); - } - } - - @SuppressWarnings("unchecked") - private static R handleResourcePatch( - R resource, - UnaryOperator updateOperation, - boolean doNotLock, - ManagedInformerEventSource ies) { - var resourceVersion = resource.getMetadata().getResourceVersion(); - try { - if (resourceVersion != null && doNotLock) { - resource.getMetadata().setResourceVersion(null); - } - return (R) ies.updateAndCacheResource(resource, updateOperation); - } finally { - if (resourceVersion != null && doNotLock) { - resource.getMetadata().setResourceVersion(resourceVersion); - } - } - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 9f5c8d93d4..84fc5b0823 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -40,7 +40,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.Controller; @@ -195,7 +195,7 @@ private PostExecutionControl

reconcileExecution( } if (updateControl.isPatchResource()) { - updatedCustomResource = patchResource(toUpdate, originalResource); + updatedCustomResource = patchResource(context, toUpdate, originalResource); if (!useSSA) { toUpdate .getMetadata() @@ -381,7 +381,7 @@ private P addFinalizerWithSSA(P originalResource) { objectMeta.setNamespace(originalResource.getMetadata().getNamespace()); resource.setMetadata(objectMeta); resource.addFinalizer(configuration().getFinalizerName()); - return customResourceFacade.patchResourceWithSSA(resource); + return customResourceFacade.simpleServerSideApply(resource); } catch (InstantiationException | IllegalAccessException | InvocationTargetException @@ -406,7 +406,7 @@ private P updateCustomResourceWithFinalizer(P resourceForExecution, P originalRe false); } - private P patchResource(P resource, P originalResource) { + private P patchResource(Context

context, P resource, P originalResource) { log.debug( "Updating resource: {} with version: {}; SSA: {}", getUID(resource), @@ -419,7 +419,7 @@ private P patchResource(P resource, P originalResource) { // addFinalizer already prevents adding an already present finalizer so no need to check resource.addFinalizer(finalizerName); } - return customResourceFacade.patchResource(resource, originalResource); + return customResourceFacade.patchResource(context, resource, originalResource); } ControllerConfiguration

configuration() { @@ -444,7 +444,7 @@ public P conflictRetryingPatch( if (forceNotUseSSA) { return customResourceFacade.patchResourceWithoutSSA(resource, originalResource); } else { - return customResourceFacade.patchResource(resource, originalResource); + return customResourceFacade.simpleServerSideApply(resource); } } catch (KubernetesClientException e) { log.trace("Exception during patch for resource: {}", resource); @@ -516,7 +516,7 @@ public R patchResourceWithoutSSA(R resource, R originalResource) { return resource(originalResource).edit(r -> resource); } - public R patchResource(R resource, R originalResource) { + public R patchResource(Context context, R resource, R originalResource) { if (log.isDebugEnabled()) { log.debug( "Trying to replace resource {}, version: {}", @@ -524,9 +524,9 @@ public R patchResource(R resource, R originalResource) { resource.getMetadata().getResourceVersion()); } if (useSSA) { - return patchResourceWithSSA(resource); + return ReconcileUtils.serverSideApplyPrimary(context, resource); } else { - return resource(originalResource).edit(r -> resource); + return ReconcileUtils.jsonPatchPrimary(context, originalResource, r -> resource); } } @@ -537,7 +537,7 @@ public R patchStatus(Context context, R resource, R originalResource) { try { resource.getMetadata().setManagedFields(null); var res = resource(resource); - return ReconcilerUtils.ssaStatusPrimary(context, resource); + return ReconcileUtils.serverSideApplyPrimaryStatus(context, resource); } finally { resource.getMetadata().setManagedFields(managedFields); } @@ -567,7 +567,7 @@ private R editStatus(R resource, R originalResource) { } } - public R patchResourceWithSSA(R resource) { + public R simpleServerSideApply(R resource) { return resource(resource) .patch( new PatchContext.Builder() diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index b0ff06d6b0..c804ad4361 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -158,7 +158,7 @@ void addFinalizerOnNewResource() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(reconciler, never()).reconcile(ArgumentMatchers.eq(testCustomResource), any()); verify(customResourceFacade, times(1)) - .patchResourceWithSSA( + .simpleServerSideApply( argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER))); } @@ -173,6 +173,7 @@ void addFinalizerOnNewResourceWithoutSSA() { verify(reconciler, never()).reconcile(ArgumentMatchers.eq(testCustomResource), any()); verify(customResourceFacade, times(1)) .patchResource( + any(), argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER)), any()); assertThat(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)).isTrue(); @@ -190,13 +191,13 @@ void patchesBothResourceAndStatusIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.patchResourceAndStatus(testCustomResource); - when(customResourceFacade.patchResource(eq(testCustomResource), any())) + when(customResourceFacade.patchResource(any(), eq(testCustomResource), any())) .thenReturn(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, times(1)).patchResource(eq(testCustomResource), any()); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchResource(any(), eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); } @Test @@ -207,8 +208,8 @@ void patchesStatus() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); - verify(customResourceFacade, never()).patchResource(any(), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); + verify(customResourceFacade, never()).patchResource(any(), any(), any()); } @Test @@ -354,7 +355,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); - verify(customResourceFacade, never()).patchResource(any(), any()); + verify(customResourceFacade, never()).patchResource(any(), any(), any()); } @Test @@ -364,21 +365,21 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, never()).patchResource(any(), any()); - verify(customResourceFacade, never()).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, never()).patchResource(any(), any(), any()); + verify(customResourceFacade, never()).patchStatus(any(), eq(testCustomResource), any()); } @Test void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { removeFinalizers(testCustomResource); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); - when(customResourceFacade.patchResourceWithSSA(any())).thenReturn(testCustomResource); + when(customResourceFacade.simpleServerSideApply(any())).thenReturn(testCustomResource); var postExecControl = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, times(1)) - .patchResourceWithSSA(argThat(a -> !a.getMetadata().getFinalizers().isEmpty())); + .simpleServerSideApply(argThat(a -> !a.getMetadata().getFinalizers().isEmpty())); assertThat(postExecControl.updateIsStatusPatch()).isFalse(); assertThat(postExecControl.getUpdatedCustomResource()).isPresent(); } @@ -390,7 +391,7 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, never()).patchResource(any(), any()); + verify(customResourceFacade, never()).patchResource(any(), any(), any()); verify(reconciler, never()).cleanup(eq(testCustomResource), any()); } @@ -471,7 +472,7 @@ void doesNotUpdatesObservedGenerationIfStatusIsNotPatchedWhenUsingSSA() throws E CustomResourceFacade facade = mock(CustomResourceFacade.class); when(config.isGenerationAware()).thenReturn(true); when(reconciler.reconcile(any(), any())).thenReturn(UpdateControl.noUpdate()); - when(facade.patchStatus(any(), any())).thenReturn(observedGenResource); + when(facade.patchStatus(any(), any(), any())).thenReturn(observedGenResource); var dispatcher = init(observedGenResource, reconciler, config, facade, true); PostExecutionControl control = @@ -489,12 +490,12 @@ void doesNotPatchObservedGenerationOnCustomResourcePatch() throws Exception { when(config.isGenerationAware()).thenReturn(true); when(reconciler.reconcile(any(), any())) .thenReturn(UpdateControl.patchResource(observedGenResource)); - when(facade.patchResource(any(), any())).thenReturn(observedGenResource); + when(facade.patchResource(any(), any(), any())).thenReturn(observedGenResource); var dispatcher = init(observedGenResource, reconciler, config, facade, false); dispatcher.handleExecution(executionScopeWithCREvent(observedGenResource)); - verify(facade, never()).patchStatus(any(), any()); + verify(facade, never()).patchStatus(any(), any(), any()); } @Test @@ -529,7 +530,7 @@ public boolean isLastAttempt() { false) .setResource(testCustomResource)); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); } @@ -550,7 +551,7 @@ void callErrorStatusHandlerEvenOnFirstError() { var postExecControl = reconciliationDispatcher.handleExecution( new ExecutionScope(null, null, false, false).setResource(testCustomResource)); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); assertThat(postExecControl.exceptionDuringExecution()).isTrue(); } @@ -573,7 +574,7 @@ void errorHandlerCanInstructNoRetryWithUpdate() { new ExecutionScope(null, null, false, false).setResource(testCustomResource)); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); assertThat(postExecControl.exceptionDuringExecution()).isFalse(); } @@ -595,7 +596,7 @@ void errorHandlerCanInstructNoRetryNoUpdate() { new ExecutionScope(null, null, false, false).setResource(testCustomResource)); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); - verify(customResourceFacade, times(0)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(0)).patchStatus(any(), eq(testCustomResource), any()); assertThat(postExecControl.exceptionDuringExecution()).isFalse(); } @@ -611,7 +612,7 @@ void errorStatusHandlerCanPatchResource() { reconciliationDispatcher.handleExecution( new ExecutionScope(null, null, false, false).setResource(testCustomResource)); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); } @@ -667,7 +668,7 @@ void retriesAddingFinalizerWithoutSSA() { removeFinalizers(testCustomResource); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); - when(customResourceFacade.patchResource(any(), any())) + when(customResourceFacade.patchResource(any(), any(), any())) .thenThrow(new KubernetesClientException(null, 409, null)) .thenReturn(testCustomResource); when(customResourceFacade.getResource(any(), any())) @@ -680,7 +681,7 @@ void retriesAddingFinalizerWithoutSSA() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, times(2)).patchResource(any(), any()); + verify(customResourceFacade, times(2)).patchResource(any(), any(), any()); } @Test From 957b37a8162ca25ff0d742d4a81a7febfb96a7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 17 Nov 2025 14:39:01 +0100 Subject: [PATCH 15/26] fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/ReconciliationDispatcher.java | 2 +- .../event/ReconciliationDispatcherTest.java | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 84fc5b0823..241986962e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -403,7 +403,7 @@ private P updateCustomResourceWithFinalizer(P resourceForExecution, P originalRe resourceForExecution, originalResource, r -> r.addFinalizer(configuration().getFinalizerName()), - false); + true); } private P patchResource(Context

context, P resource, P originalResource) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index c804ad4361..07c7164028 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -164,7 +164,7 @@ void addFinalizerOnNewResource() { @Test void addFinalizerOnNewResourceWithoutSSA() { - initConfigService(false); + initConfigService(false, false); final ReconciliationDispatcher dispatcher = init(testCustomResource, reconciler, null, customResourceFacade, true); @@ -172,11 +172,9 @@ void addFinalizerOnNewResourceWithoutSSA() { dispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(reconciler, never()).reconcile(ArgumentMatchers.eq(testCustomResource), any()); verify(customResourceFacade, times(1)) - .patchResource( - any(), - argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER)), - any()); - assertThat(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)).isTrue(); + .patchResourceWithoutSSA( + argThat(cr -> cr.hasFinalizer(DEFAULT_FINALIZER)), + argThat(cr -> !cr.hasFinalizer(DEFAULT_FINALIZER))); } @Test @@ -668,7 +666,7 @@ void retriesAddingFinalizerWithoutSSA() { removeFinalizers(testCustomResource); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); - when(customResourceFacade.patchResource(any(), any(), any())) + when(customResourceFacade.patchResourceWithoutSSA(any(), any())) .thenThrow(new KubernetesClientException(null, 409, null)) .thenReturn(testCustomResource); when(customResourceFacade.getResource(any(), any())) @@ -681,7 +679,7 @@ void retriesAddingFinalizerWithoutSSA() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, times(2)).patchResource(any(), any(), any()); + verify(customResourceFacade, times(2)).patchResourceWithoutSSA(any(), any()); } @Test From 2da91655036eae1f4a85e05f35f9bb8adb2ac657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Dec 2025 11:33:09 +0100 Subject: [PATCH 16/26] 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/ConfigurationService.java | 2 -- .../builtinresourcecleaner/BuiltInResourceCleanerIT.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 90b8ebaf7b..6ed9b7ff64 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -44,8 +44,6 @@ import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSIONS; - /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java index 812007708a..18e076e2bf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java @@ -24,7 +24,7 @@ import io.fabric8.kubernetes.api.model.Service; import io.javaoperatorsdk.annotation.Sample; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.dependent.standalonedependent.StandaloneDependentResourceIT; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; From cf2a96d526a4f009e4b58d656787fc838daa56dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Dec 2025 11:36:31 +0100 Subject: [PATCH 17/26] wip --- bootstrapper-maven-plugin/pom.xml | 2 +- micrometer-support/pom.xml | 2 +- operator-framework-bom/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- sample-operators/leader-election/pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml index 0ae72b2ff3..364495fca9 100644 --- a/bootstrapper-maven-plugin/pom.xml +++ b/bootstrapper-maven-plugin/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.0-SNAPSHOT + 5.3.0-SNAPSHOT bootstrapper diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 319eac7453..93765e4a8a 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.0-SNAPSHOT + 5.3.0-SNAPSHOT micrometer-support diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index 010934a7cc..1ffa1701f8 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk operator-framework-bom - 5.2.0-SNAPSHOT + 5.3.0-SNAPSHOT pom Operator SDK - Bill of Materials Java SDK for implementing Kubernetes operators diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 7dfd862ddc..361bb3ad53 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.0-SNAPSHOT + 5.3.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 4992f9d6bc..60c235a9ec 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.0-SNAPSHOT + 5.3.0-SNAPSHOT operator-framework-junit-5 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 031f903ee0..253907eb1e 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.0-SNAPSHOT + 5.3.0-SNAPSHOT operator-framework diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index 85658a13a6..70485a2f3e 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.0-SNAPSHOT + 5.3.0-SNAPSHOT sample-leader-election diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 5ace3c48a8..a2334ca8c6 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.0-SNAPSHOT + 5.3.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 4ce07ce912..6079d3bb71 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.0-SNAPSHOT + 5.3.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 15eab9585d..c9fe8c2d06 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.0-SNAPSHOT + 5.3.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 7976d0fc7b..e25920b7da 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.0-SNAPSHOT + 5.3.0-SNAPSHOT sample-webpage-operator From 5da6a2a4ba111fc956551609dc48625368a05193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Dec 2025 13:45:32 +0100 Subject: [PATCH 18/26] wip --- .../api/reconciler/ReconcileUtils.java | 182 ++++++++++++++++-- .../KubernetesDependentResource.java | 4 +- .../informer/ManagedInformerEventSource.java | 7 + .../ExternalStateReconciler.java | 2 +- 4 files changed, 174 insertions(+), 21 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java index 6cd6a4db69..4bb988b563 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java @@ -26,14 +26,18 @@ public class ReconcileUtils { private ReconcileUtils() {} - // todo javadoc // todo move finalizers mtehods & deprecate // todo namespace handling // todo compare resource version if multiple event sources provide the same resource - // for json patch make sense to retry for ? + // todo for json patch make sense to retry for ? public static R serverSideApply( Context context, R resource) { + return serverSideApply(context, resource, true); + } + + public static R serverSideApply( + Context context, R resource, boolean filterEvent) { return resourcePatch( context, resource, @@ -46,11 +50,17 @@ public static R serverSideApply( .withForce(true) .withFieldManager(context.getControllerConfiguration().fieldManager()) .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build())); + .build()), + filterEvent); } public static R serverSideApplyStatus( Context context, R resource) { + return serverSideApplyStatus(context, resource, true); + } + + public static R serverSideApplyStatus( + Context context, R resource, boolean filterEvent) { return resourcePatch( context, resource, @@ -64,10 +74,16 @@ public static R serverSideApplyStatus( .withForce(true) .withFieldManager(context.getControllerConfiguration().fieldManager()) .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build())); + .build()), + filterEvent); } public static

P serverSideApplyPrimary(Context

context, P resource) { + return serverSideApplyPrimary(context, resource, true); + } + + public static

P serverSideApplyPrimary( + Context

context, P resource, boolean filterEvent) { return resourcePatch( resource, r -> @@ -80,11 +96,17 @@ public static

P serverSideApplyPrimary(Context

contex .withFieldManager(context.getControllerConfiguration().fieldManager()) .withPatchType(PatchType.SERVER_SIDE_APPLY) .build()), - context.eventSourceRetriever().getControllerEventSource()); + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); } public static

P serverSideApplyPrimaryStatus( Context

context, P resource) { + return serverSideApplyPrimaryStatus(context, resource, true); + } + + public static

P serverSideApplyPrimaryStatus( + Context

context, P resource, boolean filterEvent) { return resourcePatch( resource, r -> @@ -98,59 +120,178 @@ public static

P serverSideApplyPrimaryStatus( .withFieldManager(context.getControllerConfiguration().fieldManager()) .withPatchType(PatchType.SERVER_SIDE_APPLY) .build()), - context.eventSourceRetriever().getControllerEventSource()); + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); } public static R update( Context context, R resource) { - return resourcePatch(context, resource, r -> context.getClient().resource(r).update()); + return update(context, resource, true); + } + + public static R update( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + context, resource, r -> context.getClient().resource(r).update(), filterEvent); } public static R updateStatus( Context context, R resource) { - return resourcePatch(context, resource, r -> context.getClient().resource(r).updateStatus()); + return updateStatus(context, resource, true); + } + + public static R updateStatus( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + context, resource, r -> context.getClient().resource(r).updateStatus(), filterEvent); + } + + public static R updatePrimary( + Context context, R resource) { + return updatePrimary(context, resource, true); + } + + public static R updatePrimary( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).update(), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); + } + + public static R updatePrimaryStatus( + Context context, R resource) { + return updatePrimaryStatus(context, resource, true); + } + + public static R updatePrimaryStatus( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).updateStatus(), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); } public static R jsonPatch( Context context, R resource, UnaryOperator unaryOperator) { + return jsonPatch(context, resource, unaryOperator, true); + } + + public static R jsonPatch( + Context context, + R resource, + UnaryOperator unaryOperator, + boolean filterEvent) { return resourcePatch( - context, resource, r -> context.getClient().resource(r).edit(unaryOperator)); + context, resource, r -> context.getClient().resource(r).edit(unaryOperator), filterEvent); } public static R jsonPatchStatus( Context context, R resource, UnaryOperator unaryOperator) { + return jsonPatchStatus(context, resource, unaryOperator, true); + } + + public static R jsonPatchStatus( + Context context, + R resource, + UnaryOperator unaryOperator, + boolean filterEvent) { return resourcePatch( - context, resource, r -> context.getClient().resource(r).editStatus(unaryOperator)); + context, + resource, + r -> context.getClient().resource(r).editStatus(unaryOperator), + filterEvent); } public static R jsonPatchPrimary( Context context, R resource, UnaryOperator unaryOperator) { + return jsonPatchPrimary(context, resource, unaryOperator, true); + } + + public static R jsonPatchPrimary( + Context context, + R resource, + UnaryOperator unaryOperator, + boolean filterEvent) { return resourcePatch( resource, r -> context.getClient().resource(r).edit(unaryOperator), - context.eventSourceRetriever().getControllerEventSource()); + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); } public static R jsonPatchPrimaryStatus( Context context, R resource, UnaryOperator unaryOperator) { + return jsonPatchPrimaryStatus(context, resource, unaryOperator, true); + } + + public static R jsonPatchPrimaryStatus( + Context context, + R resource, + UnaryOperator unaryOperator, + boolean filterEvent) { return resourcePatch( resource, r -> context.getClient().resource(r).editStatus(unaryOperator), - context.eventSourceRetriever().getControllerEventSource()); + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); } public static R jsonMergePatch( Context context, R resource) { - return resourcePatch(context, resource, r -> context.getClient().resource(r).patch()); + return jsonMergePatch(context, resource, true); + } + + public static R jsonMergePatch( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + context, resource, r -> context.getClient().resource(r).patch(), filterEvent); + } + + public static R jsonMergePatchStatus( + Context context, R resource) { + return jsonMergePatchStatus(context, resource, true); } public static R jsonMergePatchStatus( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + context, resource, r -> context.getClient().resource(r).patchStatus(), filterEvent); + } + + public static R jsonMergePatchPrimary( + Context context, R resource) { + return jsonMergePatchPrimary(context, resource, true); + } + + public static R jsonMergePatchPrimary( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).patch(), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); + } + + public static R jsonMergePatchPrimaryStatus( Context context, R resource) { - return resourcePatch(context, resource, r -> context.getClient().resource(r).patchStatus()); + return jsonMergePatchPrimaryStatus(context, resource, true); + } + + public static R jsonMergePatchPrimaryStatus( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).patch(), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); } public static R resourcePatch( - Context context, R resource, UnaryOperator updateOperation) { + Context context, R resource, UnaryOperator updateOperation, boolean filterEvent) { + var esList = context.eventSourceRetriever().getEventSourcesFor(resource.getClass()); if (esList.isEmpty()) { throw new IllegalStateException("No event source found for type: " + resource.getClass()); @@ -163,7 +304,7 @@ public static R resourcePatch( } var es = esList.get(0); if (es instanceof ManagedInformerEventSource mes) { - return resourcePatch(resource, updateOperation, mes); + return resourcePatch(resource, updateOperation, mes, filterEvent); } else { throw new IllegalStateException( "Target event source must be a subclass off " @@ -173,7 +314,12 @@ public static R resourcePatch( @SuppressWarnings("unchecked") public static R resourcePatch( - R resource, UnaryOperator updateOperation, ManagedInformerEventSource ies) { - return (R) ies.updateAndCacheResource(resource, updateOperation); + R resource, + UnaryOperator updateOperation, + ManagedInformerEventSource ies, + boolean filterEvent) { + return filterEvent + ? (R) ies.eventFilteringUpdateAndCacheResource(resource, updateOperation) + : (R) ies.updateAndCacheResource(resource, updateOperation); } } 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 846e3f5ceb..b9ea27b190 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 @@ -75,7 +75,7 @@ public void configureWith(KubernetesDependentResourceConfig config) { protected R handleCreate(R desired, P primary, Context

context) { return eventSource() .orElseThrow() - .updateAndCacheResource( + .eventFilteringUpdateAndCacheResource( desired, toCreate -> KubernetesDependentResource.super.handleCreate(toCreate, primary, context)); } @@ -84,7 +84,7 @@ protected R handleCreate(R desired, P primary, Context

context) { protected R handleUpdate(R actual, R desired, P primary, Context

context) { return eventSource() .orElseThrow() - .updateAndCacheResource( + .eventFilteringUpdateAndCacheResource( desired, toUpdate -> KubernetesDependentResource.super.handleUpdate(actual, toUpdate, primary, context)); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 230c649872..a45dbd8f74 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -99,6 +99,13 @@ public void changeNamespaces(Set namespaces) { } public R updateAndCacheResource(R resourceToUpdate, UnaryOperator updateMethod) { + ResourceID id = ResourceID.fromResource(resourceToUpdate); + var updated = updateMethod.apply(resourceToUpdate); + handleRecentResourceUpdate(id, updated, resourceToUpdate); + return updated; + } + + public R eventFilteringUpdateAndCacheResource(R resourceToUpdate, UnaryOperator updateMethod) { ResourceID id = ResourceID.fromResource(resourceToUpdate); if (log.isDebugEnabled()) { log.debug("Update and cache: {}", id); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java index cc574fcbe1..5a9d9a7f06 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java @@ -109,7 +109,7 @@ private void createExternalResource( // Making sure that the created resources are in the cache for the next reconciliation. // This is critical in this case, since on next reconciliation if it would not be in the cache // it would be created again. - configMapEventSource.updateAndCacheResource( + configMapEventSource.eventFilteringUpdateAndCacheResource( configMap, toCreate -> context.getClient().configMaps().resource(toCreate).create()); externalResourceEventSource.handleRecentResourceCreate(primaryID, createdResource); } From 643191f0dc66f7a45bbd32f27c53fbca7a4829e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Dec 2025 14:59:07 +0100 Subject: [PATCH 19/26] wip --- .../PrimaryUpdateAndCacheUtils.java | 41 --- .../api/reconciler/ReconcileUtils.java | 249 +++++++++++++++++- .../event/ReconciliationDispatcher.java | 144 +--------- .../informer/ManagedInformerEventSource.java | 7 +- .../informer/TemporaryResourceCache.java | 8 +- .../PrimaryUpdateAndCacheUtilsTest.java | 51 ---- .../api/reconciler/ReconcileUtilsTest.java | 79 ++++++ .../event/ReconciliationDispatcherTest.java | 115 ++++---- 8 files changed, 387 insertions(+), 307 deletions(-) create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java index 11dfd21648..6103b4b12b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java @@ -450,45 +450,4 @@ public static

P addFinalizerWithSSA( e); } } - - public static int compareResourceVersions(HasMetadata h1, HasMetadata h2) { - return compareResourceVersions( - h1.getMetadata().getResourceVersion(), h2.getMetadata().getResourceVersion()); - } - - public static int compareResourceVersions(String v1, String v2) { - int v1Length = validateResourceVersion(v1); - int v2Length = validateResourceVersion(v2); - int comparison = v1Length - v2Length; - if (comparison != 0) { - return comparison; - } - for (int i = 0; i < v2Length; i++) { - int comp = v1.charAt(i) - v2.charAt(i); - if (comp != 0) { - return comp; - } - } - return 0; - } - - private static int validateResourceVersion(String v1) { - int v1Length = v1.length(); - if (v1Length == 0) { - throw new NonComparableResourceVersionException("Resource version is empty"); - } - for (int i = 0; i < v1Length; i++) { - char char1 = v1.charAt(i); - if (char1 == '0') { - if (i == 0) { - throw new NonComparableResourceVersionException( - "Resource version cannot begin with 0: " + v1); - } - } else if (char1 < '0' || char1 > '9') { - throw new NonComparableResourceVersionException( - "Non numeric characters in resource version: " + v1); - } - } - return v1Length; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java index 4bb988b563..0e94b87aaf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java @@ -15,19 +15,36 @@ */ package io.javaoperatorsdk.operator.api.reconciler; +import java.lang.reflect.InvocationTargetException; +import java.util.function.Predicate; import java.util.function.UnaryOperator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.base.PatchContext; import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; + public class ReconcileUtils { + private static final Logger log = LoggerFactory.getLogger(ReconcileUtils.class); + + public static final int DEFAULT_MAX_RETRY = 10; + private ReconcileUtils() {} // todo move finalizers mtehods & deprecate - // todo namespace handling + // todo test namespace handling // todo compare resource version if multiple event sources provide the same resource // todo for json patch make sense to retry for ? @@ -322,4 +339,234 @@ public static R resourcePatch( ? (R) ies.eventFilteringUpdateAndCacheResource(resource, updateOperation) : (R) ies.updateAndCacheResource(resource, updateOperation); } + + public static

P addFinalizer(Context

context) { + return addFinalizer(context, context.getControllerConfiguration().getFinalizerName()); + } + + /** + * Adds finalizer to the primary resource from the context using JSON Patch. Retries conflicts and + * unprocessable content (HTTP 422), see {@link + * PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata, UnaryOperator, + * Predicate)} for details on retry. It does not add finalizer if there is already a finalizer or + * resource is marked for deletion. + * + * @return updated resource from the server response + */ + public static

P addFinalizer(Context

context, String finalizer) { + return addFinalizer(context.getClient(), context.getPrimaryResource(), finalizer); + } + + /** + * Adds finalizer to the resource using JSON Patch. Retries conflicts and unprocessable content + * (HTTP 422), see {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, + * HasMetadata, UnaryOperator, Predicate)} for details on retry. It does not try to add finalizer + * if there is already a finalizer or resource is marked for deletion. + * + * @return updated resource from the server response + */ + public static

P addFinalizer( + KubernetesClient client, P resource, String finalizerName) { + if (resource.isMarkedForDeletion() || resource.hasFinalizer(finalizerName)) { + return resource; + } + return conflictRetryingPatch( + client, + resource, + r -> { + r.addFinalizer(finalizerName); + return r; + }, + r -> !r.hasFinalizer(finalizerName)); + } + + public static

P removeFinalizer(Context

context) { + return removeFinalizer(context, context.getControllerConfiguration().getFinalizerName()); + } + + /** + * Removes the target finalizer from the primary resource from the Context. Uses JSON Patch and + * handles retries, see {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, + * HasMetadata, UnaryOperator, Predicate)} for details. It does not try to remove finalizer if + * finalizer is not present on the resource. + * + * @return updated resource from the server response + */ + public static

P removeFinalizer( + Context

context, String finalizerName) { + return removeFinalizer(context.getClient(), context.getPrimaryResource(), finalizerName); + } + + /** + * Removes the target finalizer from target resource. Uses JSON Patch and handles retries, see + * {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata, + * UnaryOperator, Predicate)} for details. It does not try to remove finalizer if finalizer is not + * present on the resource. + * + * @return updated resource from the server response + */ + public static

P removeFinalizer( + KubernetesClient client, P resource, String finalizerName) { + if (!resource.hasFinalizer(finalizerName)) { + return resource; + } + return conflictRetryingPatch( + client, + resource, + r -> { + r.removeFinalizer(finalizerName); + return r; + }, + r -> r.hasFinalizer(finalizerName)); + } + + /** + * Patches the resource using JSON Patch. In case the server responds with conflict (HTTP 409) or + * unprocessable content (HTTP 422) it retries the operation up to the maximum number defined in + * {@link PrimaryUpdateAndCacheUtils#DEFAULT_MAX_RETRY}. + * + * @param client KubernetesClient + * @param resource to update + * @param resourceChangesOperator changes to be done on the resource before update + * @param preCondition condition to check if the patch operation still needs to be performed or + * not. + * @return updated resource from the server or unchanged if the precondition does not hold. + * @param

resource type + */ + @SuppressWarnings("unchecked") + public static

P conflictRetryingPatch( + KubernetesClient client, + P resource, + UnaryOperator

resourceChangesOperator, + Predicate

preCondition) { + if (log.isDebugEnabled()) { + log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource)); + } + int retryIndex = 0; + while (true) { + try { + if (!preCondition.test(resource)) { + return resource; + } + return client.resource(resource).edit(resourceChangesOperator); + } catch (KubernetesClientException e) { + log.trace("Exception during patch for resource: {}", resource); + retryIndex++; + // only retry on conflict (409) and unprocessable content (422) which + // can happen if JSON Patch is not a valid request since there was + // a concurrent request which already removed another finalizer: + // List element removal from a list is by index in JSON Patch + // so if addressing a second finalizer but first is meanwhile removed + // it is a wrong request. + if (e.getCode() != 409 && e.getCode() != 422) { + throw e; + } + if (retryIndex >= DEFAULT_MAX_RETRY) { + throw new OperatorException( + "Exceeded maximum (" + + DEFAULT_MAX_RETRY + + ") retry attempts to patch resource: " + + ResourceID.fromResource(resource)); + } + log.debug( + "Retrying patch for resource name: {}, namespace: {}; HTTP code: {}", + resource.getMetadata().getName(), + resource.getMetadata().getNamespace(), + e.getCode()); + var operation = client.resources(resource.getClass()); + if (resource.getMetadata().getNamespace() != null) { + resource = + (P) + operation + .inNamespace(resource.getMetadata().getNamespace()) + .withName(resource.getMetadata().getName()) + .get(); + } else { + resource = (P) operation.withName(resource.getMetadata().getName()).get(); + } + } + } + } + + public static

P addFinalizerWithSSA(Context

context) { + return addFinalizerWithSSA(context, context.getControllerConfiguration().getFinalizerName()); + } + + /** + * Adds finalizer using Server-Side Apply. In the background this method creates a fresh copy of + * the target resource, setting only name, namespace and finalizer. Does not use optimistic + * locking for the patch. + * + * @return the patched resource from the server response + */ + public static

P addFinalizerWithSSA( + Context

context, String finalizerName) { + var originalResource = context.getPrimaryResource(); + if (log.isDebugEnabled()) { + log.debug( + "Adding finalizer (using SSA) for resource: {} version: {}", + getUID(originalResource), + getVersion(originalResource)); + } + try { + P resource = (P) originalResource.getClass().getConstructor().newInstance(); + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName(originalResource.getMetadata().getName()); + objectMeta.setNamespace(originalResource.getMetadata().getNamespace()); + resource.setMetadata(objectMeta); + resource.addFinalizer(finalizerName); + + return serverSideApplyPrimary(context, resource); + } catch (InstantiationException + | IllegalAccessException + | InvocationTargetException + | NoSuchMethodException e) { + throw new RuntimeException( + "Issue with creating custom resource instance with reflection." + + " Custom Resources must provide a no-arg constructor. Class: " + + originalResource.getClass().getName(), + e); + } + } + + public static int compareResourceVersions(HasMetadata h1, HasMetadata h2) { + return compareResourceVersions( + h1.getMetadata().getResourceVersion(), h2.getMetadata().getResourceVersion()); + } + + public static int compareResourceVersions(String v1, String v2) { + int v1Length = validateResourceVersion(v1); + int v2Length = validateResourceVersion(v2); + int comparison = v1Length - v2Length; + if (comparison != 0) { + return comparison; + } + for (int i = 0; i < v2Length; i++) { + int comp = v1.charAt(i) - v2.charAt(i); + if (comp != 0) { + return comp; + } + } + return 0; + } + + private static int validateResourceVersion(String v1) { + int v1Length = v1.length(); + if (v1Length == 0) { + throw new NonComparableResourceVersionException("Resource version is empty"); + } + for (int i = 0; i < v1Length; i++) { + char char1 = v1.charAt(i); + if (char1 == '0') { + if (i == 0) { + throw new NonComparableResourceVersionException( + "Resource version cannot begin with 0: " + v1); + } + } else if (char1 < '0' || char1 > '9') { + throw new NonComparableResourceVersionException( + "Non numeric characters in resource version: " + v1); + } + } + return v1Length; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 241986962e..c06093f271 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -15,9 +15,7 @@ */ package io.javaoperatorsdk.operator.processing.event; -import java.lang.reflect.InvocationTargetException; import java.net.HttpURLConnection; -import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,12 +24,9 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.Namespaced; -import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import io.fabric8.kubernetes.client.dsl.base.PatchContext; -import io.fabric8.kubernetes.client.dsl.base.PatchType; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.Cloner; @@ -120,7 +115,7 @@ && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { // checking the cleaner for all-event-mode if (!triggerOnAllEvent() && markedForDeletion) { - return handleCleanup(resourceForExecution, originalResource, context, executionScope); + return handleCleanup(resourceForExecution, context, executionScope); } else { return handleReconcile(executionScope, resourceForExecution, originalResource, context); } @@ -149,9 +144,9 @@ private PostExecutionControl

handleReconcile( */ P updatedResource; if (useSSA) { - updatedResource = addFinalizerWithSSA(originalResource); + updatedResource = ReconcileUtils.addFinalizerWithSSA(context); } else { - updatedResource = updateCustomResourceWithFinalizer(resourceForExecution, originalResource); + updatedResource = ReconcileUtils.addFinalizer(context); } return PostExecutionControl.onlyFinalizerAdded(updatedResource); } else { @@ -318,10 +313,7 @@ private void updatePostExecutionControlWithReschedule( } private PostExecutionControl

handleCleanup( - P resourceForExecution, - P originalResource, - Context

context, - ExecutionScope

executionScope) { + P resourceForExecution, Context

context, ExecutionScope

executionScope) { if (log.isDebugEnabled()) { log.debug( "Executing delete for resource: {} with version: {}", @@ -335,24 +327,7 @@ private PostExecutionControl

handleCleanup( // cleanup is finished, nothing left to be done final var finalizerName = configuration().getFinalizerName(); if (deleteControl.isRemoveFinalizer() && resourceForExecution.hasFinalizer(finalizerName)) { - P customResource = - conflictRetryingPatch( - resourceForExecution, - originalResource, - r -> { - // the operator might not be allowed to retrieve the resource on a retry, e.g. - // when its - // permissions are removed by deleting the namespace concurrently - if (r == null) { - log.warn( - "Could not remove finalizer on null resource: {} with version: {}", - getUID(resourceForExecution), - getVersion(resourceForExecution)); - return false; - } - return r.removeFinalizer(finalizerName); - }, - true); + P customResource = ReconcileUtils.removeFinalizer(context); return PostExecutionControl.customResourceFinalizerRemoved(customResource); } } @@ -368,44 +343,6 @@ private PostExecutionControl

handleCleanup( return postExecutionControl; } - @SuppressWarnings("unchecked") - private P addFinalizerWithSSA(P originalResource) { - log.debug( - "Adding finalizer (using SSA) for resource: {} version: {}", - getUID(originalResource), - getVersion(originalResource)); - try { - P resource = (P) originalResource.getClass().getConstructor().newInstance(); - ObjectMeta objectMeta = new ObjectMeta(); - objectMeta.setName(originalResource.getMetadata().getName()); - objectMeta.setNamespace(originalResource.getMetadata().getNamespace()); - resource.setMetadata(objectMeta); - resource.addFinalizer(configuration().getFinalizerName()); - return customResourceFacade.simpleServerSideApply(resource); - } catch (InstantiationException - | IllegalAccessException - | InvocationTargetException - | NoSuchMethodException e) { - throw new RuntimeException( - "Issue with creating custom resource instance with reflection." - + " Custom Resources must provide a no-arg constructor. Class: " - + originalResource.getClass().getName(), - e); - } - } - - private P updateCustomResourceWithFinalizer(P resourceForExecution, P originalResource) { - log.debug( - "Adding finalizer for resource: {} version: {}", - getUID(originalResource), - getVersion(originalResource)); - return conflictRetryingPatch( - resourceForExecution, - originalResource, - r -> r.addFinalizer(configuration().getFinalizerName()), - true); - } - private P patchResource(Context

context, P resource, P originalResource) { log.debug( "Updating resource: {} with version: {}; SSA: {}", @@ -426,57 +363,6 @@ ControllerConfiguration

configuration() { return controller.getConfiguration(); } - public P conflictRetryingPatch( - P resource, - P originalResource, - Function modificationFunction, - boolean forceNotUseSSA) { - if (log.isDebugEnabled()) { - log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource)); - } - int retryIndex = 0; - while (true) { - try { - var modified = modificationFunction.apply(resource); - if (Boolean.FALSE.equals(modified)) { - return resource; - } - if (forceNotUseSSA) { - return customResourceFacade.patchResourceWithoutSSA(resource, originalResource); - } else { - return customResourceFacade.simpleServerSideApply(resource); - } - } catch (KubernetesClientException e) { - log.trace("Exception during patch for resource: {}", resource); - retryIndex++; - // only retry on conflict (409) and unprocessable content (422) which - // can happen if JSON Patch is not a valid request since there was - // a concurrent request which already removed another finalizer: - // List element removal from a list is by index in JSON Patch - // so if addressing a second finalizer but first is meanwhile removed - // it is a wrong request. - if (e.getCode() != 409 && e.getCode() != 422) { - throw e; - } - if (retryIndex >= MAX_UPDATE_RETRY) { - throw new OperatorException( - "Exceeded maximum (" - + MAX_UPDATE_RETRY - + ") retry attempts to patch resource: " - + ResourceID.fromResource(resource)); - } - log.debug( - "Retrying patch for resource name: {}, namespace: {}; HTTP code: {}", - resource.getMetadata().getName(), - resource.getMetadata().getNamespace(), - e.getCode()); - resource = - customResourceFacade.getResource( - resource.getMetadata().getNamespace(), resource.getMetadata().getName()); - } - } - } - private void validateExecutionScope(ExecutionScope

executionScope) { if (!triggerOnAllEvent() && (executionScope.isDeleteEvent() || executionScope.isDeleteFinalStateUnknown())) { @@ -536,17 +422,16 @@ public R patchStatus(Context context, R resource, R originalResource) { var managedFields = resource.getMetadata().getManagedFields(); try { resource.getMetadata().setManagedFields(null); - var res = resource(resource); return ReconcileUtils.serverSideApplyPrimaryStatus(context, resource); } finally { resource.getMetadata().setManagedFields(managedFields); } } else { - return editStatus(resource, originalResource); + return editStatus(context, resource, originalResource); } } - private R editStatus(R resource, R originalResource) { + private R editStatus(Context context, R resource, R originalResource) { String resourceVersion = resource.getMetadata().getResourceVersion(); // the cached resource should not be changed in any circumstances // that can lead to all kinds of race conditions. @@ -554,8 +439,9 @@ private R editStatus(R resource, R originalResource) { try { clonedOriginal.getMetadata().setResourceVersion(null); resource.getMetadata().setResourceVersion(null); - var res = resource(clonedOriginal); - return res.editStatus( + return ReconcileUtils.jsonPatchPrimaryStatus( + context, + originalResource, r -> { ReconcilerUtilsInternal.setStatus(r, ReconcilerUtilsInternal.getStatus(resource)); return r; @@ -567,16 +453,6 @@ private R editStatus(R resource, R originalResource) { } } - public R simpleServerSideApply(R resource) { - return resource(resource) - .patch( - new PatchContext.Builder() - .withFieldManager(fieldManager) - .withForce(true) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build()); - } - private Resource resource(R resource) { return resource instanceof Namespaced ? resourceOperation.inNamespace(resource.getMetadata().getNamespace()).resource(resource) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index a45dbd8f74..0ade9a44c0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -35,7 +35,7 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Informable; import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; -import io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils; +import io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils; import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; import io.javaoperatorsdk.operator.health.InformerHealthIndicator; import io.javaoperatorsdk.operator.health.InformerWrappingEventSourceHealthIndicator; @@ -160,10 +160,7 @@ public Optional get(ResourceID resourceID) { Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); if (comparableResourceVersions && resource.isPresent() - && res.filter( - r -> - PrimaryUpdateAndCacheUtils.compareResourceVersions(r, resource.orElseThrow()) - > 0) + && res.filter(r -> ReconcileUtils.compareResourceVersions(r, resource.orElseThrow()) > 0) .isEmpty()) { log.debug("Latest resource found in temporary cache for Resource ID: {}", resourceID); return resource; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index d918be447d..d41706d702 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -24,7 +24,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils; +import io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -117,7 +117,7 @@ private boolean onEvent(T resource, boolean unknownState) { (id, cached) -> { boolean remove = unknownState; if (!unknownState) { - int comp = PrimaryUpdateAndCacheUtils.compareResourceVersions(resource, cached); + int comp = ReconcileUtils.compareResourceVersions(resource, cached); if (comp >= 0) { remove = true; } @@ -157,7 +157,7 @@ public synchronized void putResource(T newResource) { // this also prevents resurrecting recently deleted entities for which the delete event // has already been processed if (latestResourceVersion != null - && PrimaryUpdateAndCacheUtils.compareResourceVersions( + && ReconcileUtils.compareResourceVersions( latestResourceVersion, newResource.getMetadata().getResourceVersion()) > 0) { log.debug( @@ -172,7 +172,7 @@ public synchronized void putResource(T newResource) { var cachedResource = getResourceFromCache(resourceId).orElse(null); if (cachedResource == null - || PrimaryUpdateAndCacheUtils.compareResourceVersions(newResource, cachedResource) > 0) { + || ReconcileUtils.compareResourceVersions(newResource, cachedResource) > 0) { log.debug( "Temporarily moving ahead to target version {} for resource id: {}", newResource.getMetadata().getResourceVersion(), diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java index c878a4fc06..235dd3cd40 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtilsTest.java @@ -19,7 +19,6 @@ import java.util.function.UnaryOperator; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,7 +39,6 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils.DEFAULT_MAX_RETRY; -import static io.javaoperatorsdk.operator.api.reconciler.PrimaryUpdateAndCacheUtils.compareResourceVersions; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -182,53 +180,4 @@ void cachePollTimeouts() { 10L)); assertThat(ex.getMessage()).contains("Timeout"); } - - @Test - public void compareResourceVersionsTest() { - assertThat(compareResourceVersions("11", "22")).isNegative(); - assertThat(compareResourceVersions("22", "11")).isPositive(); - assertThat(compareResourceVersions("1", "1")).isZero(); - assertThat(compareResourceVersions("11", "11")).isZero(); - assertThat(compareResourceVersions("123", "2")).isPositive(); - assertThat(compareResourceVersions("3", "211")).isNegative(); - - assertThrows( - NonComparableResourceVersionException.class, () -> compareResourceVersions("aa", "22")); - assertThrows( - NonComparableResourceVersionException.class, () -> compareResourceVersions("11", "ba")); - assertThrows( - NonComparableResourceVersionException.class, () -> compareResourceVersions("", "22")); - assertThrows( - NonComparableResourceVersionException.class, () -> compareResourceVersions("11", "")); - assertThrows( - NonComparableResourceVersionException.class, () -> compareResourceVersions("01", "123")); - assertThrows( - NonComparableResourceVersionException.class, () -> compareResourceVersions("123", "01")); - assertThrows( - NonComparableResourceVersionException.class, () -> compareResourceVersions("3213", "123a")); - assertThrows( - NonComparableResourceVersionException.class, () -> compareResourceVersions("321", "123a")); - } - - // naive performance test that compares the work case scenario for the parsing and non-parsing - // variants - @Test - @Disabled - public void compareResourcePerformanceTest() { - var execNum = 30000000; - var startTime = System.currentTimeMillis(); - for (int i = 0; i < execNum; i++) { - var res = compareResourceVersions("123456788", "123456789"); - } - var dur1 = System.currentTimeMillis() - startTime; - log.info("Duration without parsing: {}", dur1); - startTime = System.currentTimeMillis(); - for (int i = 0; i < execNum; i++) { - var res = Long.parseLong("123456788") > Long.parseLong("123456789"); - } - var dur2 = System.currentTimeMillis() - startTime; - log.info("Duration with parsing: {}", dur2); - - assertThat(dur1).isLessThan(dur2); - } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java new file mode 100644 index 0000000000..675de44a4c --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java @@ -0,0 +1,79 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.api.reconciler; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils.compareResourceVersions; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class ReconcileUtilsTest { + + private static final Logger log = LoggerFactory.getLogger(ReconcileUtilsTest.class); + + @Test + public void compareResourceVersionsTest() { + assertThat(compareResourceVersions("11", "22")).isNegative(); + assertThat(compareResourceVersions("22", "11")).isPositive(); + assertThat(compareResourceVersions("1", "1")).isZero(); + assertThat(compareResourceVersions("11", "11")).isZero(); + assertThat(compareResourceVersions("123", "2")).isPositive(); + assertThat(compareResourceVersions("3", "211")).isNegative(); + + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("aa", "22")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("11", "ba")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("", "22")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("11", "")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("01", "123")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("123", "01")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("3213", "123a")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("321", "123a")); + } + + // naive performance test that compares the work case scenario for the parsing and non-parsing + // variants + @Test + @Disabled + public void compareResourcePerformanceTest() { + var execNum = 30000000; + var startTime = System.currentTimeMillis(); + for (int i = 0; i < execNum; i++) { + var res = compareResourceVersions("123456788", "123456789"); + } + var dur1 = System.currentTimeMillis() - startTime; + log.info("Duration without parsing: {}", dur1); + startTime = System.currentTimeMillis(); + for (int i = 0; i < execNum; i++) { + var res = Long.parseLong("123456788") > Long.parseLong("123456789"); + } + var dur2 = System.currentTimeMillis() - startTime; + log.info("Duration with parsing: {}", dur2); + + assertThat(dur1).isLessThan(dur2); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index 07c7164028..460496890a 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -23,9 +23,11 @@ import java.util.function.Supplier; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; import org.mockito.stubbing.Answer; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -46,6 +48,7 @@ import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @@ -56,10 +59,8 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.TestUtils.markForDeletion; -import static io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.MAX_UPDATE_RETRY; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*; @SuppressWarnings({"unchecked", "rawtypes"}) @@ -154,27 +155,26 @@ public boolean useFinalizer() { @Test void addFinalizerOnNewResource() { - assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(reconciler, never()).reconcile(ArgumentMatchers.eq(testCustomResource), any()); - verify(customResourceFacade, times(1)) - .simpleServerSideApply( - argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER))); + try (MockedStatic mockedReconcileUtils = mockStatic(ReconcileUtils.class)) { + assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + verify(reconciler, never()).reconcile(ArgumentMatchers.eq(testCustomResource), any()); + mockedReconcileUtils.verify(() -> ReconcileUtils.addFinalizerWithSSA(any()), times(1)); + } } @Test void addFinalizerOnNewResourceWithoutSSA() { - initConfigService(false, false); - final ReconciliationDispatcher dispatcher = - init(testCustomResource, reconciler, null, customResourceFacade, true); - - assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); - dispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(reconciler, never()).reconcile(ArgumentMatchers.eq(testCustomResource), any()); - verify(customResourceFacade, times(1)) - .patchResourceWithoutSSA( - argThat(cr -> cr.hasFinalizer(DEFAULT_FINALIZER)), - argThat(cr -> !cr.hasFinalizer(DEFAULT_FINALIZER))); + try (MockedStatic mockedReconcileUtils = mockStatic(ReconcileUtils.class)) { + initConfigService(false, false); + final ReconciliationDispatcher dispatcher = + init(testCustomResource, reconciler, null, customResourceFacade, true); + + assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); + dispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + verify(reconciler, never()).reconcile(ArgumentMatchers.eq(testCustomResource), any()); + mockedReconcileUtils.verify(() -> ReconcileUtils.addFinalizer(any()), times(1)); + } } @Test @@ -231,16 +231,19 @@ void callsDeleteIfObjectHasFinalizerAndMarkedForDelete() { @Test void removesDefaultFinalizerOnDeleteIfSet() { - testCustomResource.addFinalizer(DEFAULT_FINALIZER); - markForDeletion(testCustomResource); + try (MockedStatic mockedReconcileUtils = mockStatic(ReconcileUtils.class)) { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + markForDeletion(testCustomResource); - var postExecControl = - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + var postExecControl = + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - assertThat(postExecControl.isFinalizerRemoved()).isTrue(); - verify(customResourceFacade, times(1)).patchResourceWithoutSSA(eq(testCustomResource), any()); + assertThat(postExecControl.isFinalizerRemoved()).isTrue(); + mockedReconcileUtils.verify(() -> ReconcileUtils.removeFinalizer(any()), times(1)); + } } + @Disabled("todo move to ReconcileUtils test") @Test void retriesFinalizerRemovalWithFreshResource() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); @@ -260,6 +263,8 @@ void retriesFinalizerRemovalWithFreshResource() { verify(customResourceFacade, times(1)).getResource(any(), any()); } + // TODO move to utils test + @Disabled @Test void nullResourceIsGracefullyHandledOnFinalizerRemovalRetry() { // simulate the operator not able or not be allowed to get the custom resource during the retry @@ -278,41 +283,6 @@ void nullResourceIsGracefullyHandledOnFinalizerRemovalRetry() { verify(customResourceFacade, times(1)).getResource(any(), any()); } - @Test - void throwsExceptionIfFinalizerRemovalRetryExceeded() { - testCustomResource.addFinalizer(DEFAULT_FINALIZER); - markForDeletion(testCustomResource); - when(customResourceFacade.patchResourceWithoutSSA(any(), any())) - .thenThrow(new KubernetesClientException(null, 409, null)); - when(customResourceFacade.getResource(any(), any())) - .thenAnswer((Answer) invocationOnMock -> createResourceWithFinalizer()); - - var postExecControl = - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - - assertThat(postExecControl.isFinalizerRemoved()).isFalse(); - assertThat(postExecControl.getRuntimeException()).isPresent(); - assertThat(postExecControl.getRuntimeException().get()).isInstanceOf(OperatorException.class); - verify(customResourceFacade, times(MAX_UPDATE_RETRY)).patchResourceWithoutSSA(any(), any()); - verify(customResourceFacade, times(MAX_UPDATE_RETRY - 1)).getResource(any(), any()); - } - - @Test - void throwsExceptionIfFinalizerRemovalClientExceptionIsNotConflict() { - testCustomResource.addFinalizer(DEFAULT_FINALIZER); - markForDeletion(testCustomResource); - when(customResourceFacade.patchResourceWithoutSSA(any(), any())) - .thenThrow(new KubernetesClientException(null, 400, null)); - - var res = - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - - assertThat(res.getRuntimeException()).isPresent(); - assertThat(res.getRuntimeException().get()).isInstanceOf(KubernetesClientException.class); - verify(customResourceFacade, times(1)).patchResourceWithoutSSA(any(), any()); - verify(customResourceFacade, never()).getResource(any(), any()); - } - @Test void doesNotCallDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { final ReconciliationDispatcher dispatcher = @@ -369,17 +339,19 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { @Test void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { - removeFinalizers(testCustomResource); - reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); - when(customResourceFacade.simpleServerSideApply(any())).thenReturn(testCustomResource); - - var postExecControl = - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - - verify(customResourceFacade, times(1)) - .simpleServerSideApply(argThat(a -> !a.getMetadata().getFinalizers().isEmpty())); - assertThat(postExecControl.updateIsStatusPatch()).isFalse(); - assertThat(postExecControl.getUpdatedCustomResource()).isPresent(); + try (MockedStatic mockedReconcileUtils = mockStatic(ReconcileUtils.class)) { + removeFinalizers(testCustomResource); + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); + mockedReconcileUtils + .when(() -> ReconcileUtils.addFinalizerWithSSA(any())) + .thenReturn(testCustomResource); + var postExecControl = + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + + mockedReconcileUtils.verify(() -> ReconcileUtils.addFinalizerWithSSA(any()), times(1)); + assertThat(postExecControl.updateIsStatusPatch()).isFalse(); + assertThat(postExecControl.getUpdatedCustomResource()).isPresent(); + } } @Test @@ -659,6 +631,7 @@ void canSkipSchedulingMaxDelayIf() { } @Test + @Disabled("Move to reconciler utils test") void retriesAddingFinalizerWithoutSSA() { initConfigService(false); reconciliationDispatcher = From b9bddab355fdad236fde8936b3f60f91d533382b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Dec 2025 15:50:03 +0100 Subject: [PATCH 20/26] wip --- .../api/reconciler/ReconcileUtils.java | 493 ++++++++++++++++-- 1 file changed, 454 insertions(+), 39 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java index 0e94b87aaf..8c240fabf7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java @@ -46,13 +46,40 @@ private ReconcileUtils() {} // todo move finalizers mtehods & deprecate // todo test namespace handling // todo compare resource version if multiple event sources provide the same resource - // todo for json patch make sense to retry for ? + /** + * Server-Side Apply the resource and filters out the resulting event. This is a convenience + * method that calls {@link #serverSideApply(Context, HasMetadata, boolean)} with filterEvent set + * to true. + * + * @param context of reconciler + * @param resource fresh resource for server side apply + * @return updated resource + * @param resource type + * @see #serverSideApply(Context, HasMetadata, boolean) + */ public static R serverSideApply( Context context, R resource) { return serverSideApply(context, resource, true); } + /** + * Updates the resource and caches the response if needed, thus making sure that next + * reconciliation will contain to updated resource. Or more recent one if someone did an update + * after our update. + * + *

Optionally also can filter out the event, what is the result of this update. + * + *

You are free to control the optimistic locking by setting the resource version in resource + * metadata. In case of SSA we advise not to do updates with optimistic locking. + * + * @param context of reconciler + * @param resource fresh resource for server side apply + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R serverSideApply( Context context, R resource, boolean filterEvent) { return resourcePatch( @@ -71,11 +98,33 @@ public static R serverSideApply( filterEvent); } + /** + * Server-Side Apply the resource status subresource and filters out the resulting event. This is + * a convenience method that calls {@link #serverSideApplyStatus(Context, HasMetadata, boolean)} + * with filterEvent set to true. + * + * @param context of reconciler + * @param resource fresh resource for server side apply + * @return updated resource + * @param resource type + * @see #serverSideApplyStatus(Context, HasMetadata, boolean) + */ public static R serverSideApplyStatus( Context context, R resource) { return serverSideApplyStatus(context, resource, true); } + /** + * Server-Side Apply the resource status subresource. Updates the resource status and caches the + * response if needed, ensuring the next reconciliation will contain the updated resource. + * + * @param context of reconciler + * @param resource fresh resource for server side apply + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R serverSideApplyStatus( Context context, R resource, boolean filterEvent) { return resourcePatch( @@ -95,10 +144,33 @@ public static R serverSideApplyStatus( filterEvent); } + /** + * Server-Side Apply the primary resource and filters out the resulting event. This is a + * convenience method that calls {@link #serverSideApplyPrimary(Context, HasMetadata, boolean)} + * with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource for server side apply + * @return updated resource + * @param

primary resource type + * @see #serverSideApplyPrimary(Context, HasMetadata, boolean) + */ public static

P serverSideApplyPrimary(Context

context, P resource) { return serverSideApplyPrimary(context, resource, true); } + /** + * Server-Side Apply the primary resource. Updates the primary resource and caches the response + * using the controller's event source, ensuring the next reconciliation will contain the updated + * resource. + * + * @param context of reconciler + * @param resource primary resource for server side apply + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param

primary resource type + */ public static

P serverSideApplyPrimary( Context

context, P resource, boolean filterEvent) { return resourcePatch( @@ -117,11 +189,33 @@ public static

P serverSideApplyPrimary( filterEvent); } + /** + * Server-Side Apply the primary resource status subresource and filters out the resulting event. + * This is a convenience method that calls {@link #serverSideApplyPrimaryStatus(Context, + * HasMetadata, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource for server side apply + * @return updated resource + * @param

primary resource type + * @see #serverSideApplyPrimaryStatus(Context, HasMetadata, boolean) + */ public static

P serverSideApplyPrimaryStatus( Context

context, P resource) { return serverSideApplyPrimaryStatus(context, resource, true); } + /** + * Server-Side Apply the primary resource status subresource. Updates the primary resource status + * and caches the response using the controller's event source. + * + * @param context of reconciler + * @param resource primary resource for server side apply + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param

primary resource type + */ public static

P serverSideApplyPrimaryStatus( Context

context, P resource, boolean filterEvent) { return resourcePatch( @@ -141,33 +235,97 @@ public static

P serverSideApplyPrimaryStatus( filterEvent); } + /** + * Updates the resource and filters out the resulting event. This is a convenience method that + * calls {@link #update(Context, HasMetadata, boolean)} with filterEvent set to true. Uses + * optimistic locking based on the resource version. + * + * @param context of reconciler + * @param resource resource to update + * @return updated resource + * @param resource type + * @see #update(Context, HasMetadata, boolean) + */ public static R update( Context context, R resource) { return update(context, resource, true); } + /** + * Updates the resource with optimistic locking based on the resource version. Caches the response + * if needed, ensuring the next reconciliation will contain the updated resource. + * + * @param context of reconciler + * @param resource resource to update + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R update( Context context, R resource, boolean filterEvent) { return resourcePatch( context, resource, r -> context.getClient().resource(r).update(), filterEvent); } + /** + * Updates the resource status subresource and filters out the resulting event. This is a + * convenience method that calls {@link #updateStatus(Context, HasMetadata, boolean)} with + * filterEvent set to true. + * + * @param context of reconciler + * @param resource resource to update + * @return updated resource + * @param resource type + * @see #updateStatus(Context, HasMetadata, boolean) + */ public static R updateStatus( Context context, R resource) { return updateStatus(context, resource, true); } + /** + * Updates the resource status subresource with optimistic locking. Caches the response if needed. + * + * @param context of reconciler + * @param resource resource to update + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R updateStatus( Context context, R resource, boolean filterEvent) { return resourcePatch( context, resource, r -> context.getClient().resource(r).updateStatus(), filterEvent); } + /** + * Updates the primary resource and filters out the resulting event. This is a convenience method + * that calls {@link #updatePrimary(Context, HasMetadata, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to update + * @return updated resource + * @param resource type + * @see #updatePrimary(Context, HasMetadata, boolean) + */ public static R updatePrimary( Context context, R resource) { return updatePrimary(context, resource, true); } + /** + * Updates the primary resource with optimistic locking. Caches the response using the + * controller's event source. + * + * @param context of reconciler + * @param resource primary resource to update + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R updatePrimary( Context context, R resource, boolean filterEvent) { return resourcePatch( @@ -177,11 +335,33 @@ public static R updatePrimary( filterEvent); } + /** + * Updates the primary resource status subresource and filters out the resulting event. This is a + * convenience method that calls {@link #updatePrimaryStatus(Context, HasMetadata, boolean)} with + * filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to update + * @return updated resource + * @param resource type + * @see #updatePrimaryStatus(Context, HasMetadata, boolean) + */ public static R updatePrimaryStatus( Context context, R resource) { return updatePrimaryStatus(context, resource, true); } + /** + * Updates the primary resource status subresource with optimistic locking. Caches the response + * using the controller's event source. + * + * @param context of reconciler + * @param resource primary resource to update + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R updatePrimaryStatus( Context context, R resource, boolean filterEvent) { return resourcePatch( @@ -191,11 +371,35 @@ public static R updatePrimaryStatus( filterEvent); } + /** + * Applies a JSON Patch to the resource and filters out the resulting event. This is a convenience + * method that calls {@link #jsonPatch(Context, HasMetadata, UnaryOperator, boolean)} with + * filterEvent set to true. + * + * @param context of reconciler + * @param resource resource to patch + * @param unaryOperator function to modify the resource + * @return updated resource + * @param resource type + * @see #jsonPatch(Context, HasMetadata, UnaryOperator, boolean) + */ public static R jsonPatch( Context context, R resource, UnaryOperator unaryOperator) { return jsonPatch(context, resource, unaryOperator, true); } + /** + * Applies a JSON Patch to the resource. The unaryOperator function is used to modify the + * resource, and the differences are sent as a JSON Patch to the Kubernetes API server. + * + * @param context of reconciler + * @param resource resource to patch + * @param unaryOperator function to modify the resource + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R jsonPatch( Context context, R resource, @@ -205,11 +409,35 @@ public static R jsonPatch( context, resource, r -> context.getClient().resource(r).edit(unaryOperator), filterEvent); } + /** + * Applies a JSON Patch to the resource status subresource and filters out the resulting event. + * This is a convenience method that calls {@link #jsonPatchStatus(Context, HasMetadata, + * UnaryOperator, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource resource to patch + * @param unaryOperator function to modify the resource + * @return updated resource + * @param resource type + * @see #jsonPatchStatus(Context, HasMetadata, UnaryOperator, boolean) + */ public static R jsonPatchStatus( Context context, R resource, UnaryOperator unaryOperator) { return jsonPatchStatus(context, resource, unaryOperator, true); } + /** + * Applies a JSON Patch to the resource status subresource. The unaryOperator function is used to + * modify the resource status, and the differences are sent as a JSON Patch. + * + * @param context of reconciler + * @param resource resource to patch + * @param unaryOperator function to modify the resource + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R jsonPatchStatus( Context context, R resource, @@ -222,11 +450,35 @@ public static R jsonPatchStatus( filterEvent); } + /** + * Applies a JSON Patch to the primary resource and filters out the resulting event. This is a + * convenience method that calls {@link #jsonPatchPrimary(Context, HasMetadata, UnaryOperator, + * boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param unaryOperator function to modify the resource + * @return updated resource + * @param resource type + * @see #jsonPatchPrimary(Context, HasMetadata, UnaryOperator, boolean) + */ public static R jsonPatchPrimary( Context context, R resource, UnaryOperator unaryOperator) { return jsonPatchPrimary(context, resource, unaryOperator, true); } + /** + * Applies a JSON Patch to the primary resource. Caches the response using the controller's event + * source. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param unaryOperator function to modify the resource + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R jsonPatchPrimary( Context context, R resource, @@ -239,11 +491,35 @@ public static R jsonPatchPrimary( filterEvent); } + /** + * Applies a JSON Patch to the primary resource status subresource and filters out the resulting + * event. This is a convenience method that calls {@link #jsonPatchPrimaryStatus(Context, + * HasMetadata, UnaryOperator, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param unaryOperator function to modify the resource + * @return updated resource + * @param resource type + * @see #jsonPatchPrimaryStatus(Context, HasMetadata, UnaryOperator, boolean) + */ public static R jsonPatchPrimaryStatus( Context context, R resource, UnaryOperator unaryOperator) { return jsonPatchPrimaryStatus(context, resource, unaryOperator, true); } + /** + * Applies a JSON Patch to the primary resource status subresource. Caches the response using the + * controller's event source. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param unaryOperator function to modify the resource + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R jsonPatchPrimaryStatus( Context context, R resource, @@ -256,33 +532,100 @@ public static R jsonPatchPrimaryStatus( filterEvent); } + /** + * Applies a JSON Merge Patch to the resource and filters out the resulting event. This is a + * convenience method that calls {@link #jsonMergePatch(Context, HasMetadata, boolean)} with + * filterEvent set to true. JSON Merge Patch (RFC 7386) is a simpler patching strategy compared to + * JSON Patch. + * + * @param context of reconciler + * @param resource resource to patch + * @return updated resource + * @param resource type + * @see #jsonMergePatch(Context, HasMetadata, boolean) + */ public static R jsonMergePatch( Context context, R resource) { return jsonMergePatch(context, resource, true); } + /** + * Applies a JSON Merge Patch to the resource. JSON Merge Patch (RFC 7386) is a simpler patching + * strategy that merges the provided resource with the existing resource on the server. + * + * @param context of reconciler + * @param resource resource to patch + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R jsonMergePatch( Context context, R resource, boolean filterEvent) { return resourcePatch( context, resource, r -> context.getClient().resource(r).patch(), filterEvent); } + /** + * Applies a JSON Merge Patch to the resource status subresource and filters out the resulting + * event. This is a convenience method that calls {@link #jsonMergePatchStatus(Context, + * HasMetadata, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource resource to patch + * @return updated resource + * @param resource type + * @see #jsonMergePatchStatus(Context, HasMetadata, boolean) + */ public static R jsonMergePatchStatus( Context context, R resource) { return jsonMergePatchStatus(context, resource, true); } + /** + * Applies a JSON Merge Patch to the resource status subresource. Merges the provided resource + * status with the existing resource status on the server. + * + * @param context of reconciler + * @param resource resource to patch + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R jsonMergePatchStatus( Context context, R resource, boolean filterEvent) { return resourcePatch( context, resource, r -> context.getClient().resource(r).patchStatus(), filterEvent); } + /** + * Applies a JSON Merge Patch to the primary resource and filters out the resulting event. This is + * a convenience method that calls {@link #jsonMergePatchPrimary(Context, HasMetadata, boolean)} + * with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to patch + * @return updated resource + * @param resource type + * @see #jsonMergePatchPrimary(Context, HasMetadata, boolean) + */ public static R jsonMergePatchPrimary( Context context, R resource) { return jsonMergePatchPrimary(context, resource, true); } + /** + * Applies a JSON Merge Patch to the primary resource. Caches the response using the controller's + * event source. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R jsonMergePatchPrimary( Context context, R resource, boolean filterEvent) { return resourcePatch( @@ -292,11 +635,33 @@ public static R jsonMergePatchPrimary( filterEvent); } + /** + * Applies a JSON Merge Patch to the primary resource status subresource and filters out the + * resulting event. This is a convenience method that calls {@link + * #jsonMergePatchPrimaryStatus(Context, HasMetadata, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to patch + * @return updated resource + * @param resource type + * @see #jsonMergePatchPrimaryStatus(Context, HasMetadata, boolean) + */ public static R jsonMergePatchPrimaryStatus( Context context, R resource) { return jsonMergePatchPrimaryStatus(context, resource, true); } + /** + * Applies a JSON Merge Patch to the primary resource status subresource. Caches the response + * using the controller's event source. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ public static R jsonMergePatchPrimaryStatus( Context context, R resource, boolean filterEvent) { return resourcePatch( @@ -306,6 +671,19 @@ public static R jsonMergePatchPrimaryStatus( filterEvent); } + /** + * Internal utility method to patch a resource and cache the result. Automatically discovers the + * event source for the resource type and delegates to {@link #resourcePatch(HasMetadata, + * UnaryOperator, ManagedInformerEventSource, boolean)}. + * + * @param context of reconciler + * @param resource resource to patch + * @param updateOperation operation to perform (update, patch, edit, etc.) + * @param filterEvent if true the event from this update will be filtered out + * @return updated resource + * @param resource type + * @throws IllegalStateException if no event source or multiple event sources are found + */ public static R resourcePatch( Context context, R resource, UnaryOperator updateOperation, boolean filterEvent) { @@ -329,6 +707,18 @@ public static R resourcePatch( } } + /** + * Internal utility method to patch a resource and cache the result using the specified event + * source. This method either filters out the resulting event or allows it to trigger + * reconciliation based on the filterEvent parameter. + * + * @param resource resource to patch + * @param updateOperation operation to perform (update, patch, edit, etc.) + * @param ies the managed informer event source to use for caching + * @param filterEvent if true the event from this update will be filtered out + * @return updated resource + * @param resource type + */ @SuppressWarnings("unchecked") public static R resourcePatch( R resource, @@ -340,21 +730,18 @@ public static R resourcePatch( : (R) ies.updateAndCacheResource(resource, updateOperation); } - public static

P addFinalizer(Context

context) { - return addFinalizer(context, context.getControllerConfiguration().getFinalizerName()); - } - /** - * Adds finalizer to the primary resource from the context using JSON Patch. Retries conflicts and - * unprocessable content (HTTP 422), see {@link - * PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata, UnaryOperator, - * Predicate)} for details on retry. It does not add finalizer if there is already a finalizer or - * resource is marked for deletion. + * Adds the default finalizer (from controller configuration) to the primary resource. This is a + * convenience method that calls {@link #addFinalizer(Context, String)} with the configured + * finalizer name. * + * @param context of reconciler * @return updated resource from the server response + * @param

primary resource type + * @see #addFinalizer(Context, String) */ - public static

P addFinalizer(Context

context, String finalizer) { - return addFinalizer(context.getClient(), context.getPrimaryResource(), finalizer); + public static

P addFinalizer(Context

context) { + return addFinalizer(context, context.getControllerConfiguration().getFinalizerName()); } /** @@ -365,14 +752,13 @@ public static

P addFinalizer(Context

context, String * * @return updated resource from the server response */ - public static

P addFinalizer( - KubernetesClient client, P resource, String finalizerName) { + public static

P addFinalizer(Context

context, String finalizerName) { + var resource = context.getPrimaryResource(); if (resource.isMarkedForDeletion() || resource.hasFinalizer(finalizerName)) { return resource; } return conflictRetryingPatch( - client, - resource, + context, r -> { r.addFinalizer(finalizerName); return r; @@ -380,21 +766,18 @@ public static

P addFinalizer( r -> !r.hasFinalizer(finalizerName)); } - public static

P removeFinalizer(Context

context) { - return removeFinalizer(context, context.getControllerConfiguration().getFinalizerName()); - } - /** - * Removes the target finalizer from the primary resource from the Context. Uses JSON Patch and - * handles retries, see {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, - * HasMetadata, UnaryOperator, Predicate)} for details. It does not try to remove finalizer if - * finalizer is not present on the resource. + * Removes the default finalizer (from controller configuration) from the primary resource. This + * is a convenience method that calls {@link #removeFinalizer(Context, String)} with the + * configured finalizer name. * + * @param context of reconciler * @return updated resource from the server response + * @param

primary resource type + * @see #removeFinalizer(Context, String) */ - public static

P removeFinalizer( - Context

context, String finalizerName) { - return removeFinalizer(context.getClient(), context.getPrimaryResource(), finalizerName); + public static

P removeFinalizer(Context

context) { + return removeFinalizer(context, context.getControllerConfiguration().getFinalizerName()); } /** @@ -406,13 +789,13 @@ public static

P removeFinalizer( * @return updated resource from the server response */ public static

P removeFinalizer( - KubernetesClient client, P resource, String finalizerName) { + Context

context, String finalizerName) { + var resource = context.getPrimaryResource(); if (!resource.hasFinalizer(finalizerName)) { return resource; } return conflictRetryingPatch( - client, - resource, + context, r -> { r.removeFinalizer(finalizerName); return r; @@ -425,8 +808,7 @@ public static

P removeFinalizer( * unprocessable content (HTTP 422) it retries the operation up to the maximum number defined in * {@link PrimaryUpdateAndCacheUtils#DEFAULT_MAX_RETRY}. * - * @param client KubernetesClient - * @param resource to update + * @param context reconiciliation context * @param resourceChangesOperator changes to be done on the resource before update * @param preCondition condition to check if the patch operation still needs to be performed or * not. @@ -434,11 +816,10 @@ public static

P removeFinalizer( * @param

resource type */ @SuppressWarnings("unchecked") - public static

P conflictRetryingPatch( - KubernetesClient client, - P resource, - UnaryOperator

resourceChangesOperator, - Predicate

preCondition) { + private static

P conflictRetryingPatch( + Context

context, UnaryOperator

resourceChangesOperator, Predicate

preCondition) { + var resource = context.getPrimaryResource(); + var client = context.getClient(); if (log.isDebugEnabled()) { log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource)); } @@ -448,7 +829,7 @@ public static

P conflictRetryingPatch( if (!preCondition.test(resource)) { return resource; } - return client.resource(resource).edit(resourceChangesOperator); + return jsonPatchPrimary(context, resource, resourceChangesOperator, false); } catch (KubernetesClientException e) { log.trace("Exception during patch for resource: {}", resource); retryIndex++; @@ -488,6 +869,16 @@ public static

P conflictRetryingPatch( } } + /** + * Adds the default finalizer (from controller configuration) to the primary resource using + * Server-Side Apply. This is a convenience method that calls {@link #addFinalizerWithSSA(Context, + * String)} with the configured finalizer name. + * + * @param context of reconciler + * @return the patched resource from the server response + * @param

primary resource type + * @see #addFinalizerWithSSA(Context, String) + */ public static

P addFinalizerWithSSA(Context

context) { return addFinalizerWithSSA(context, context.getControllerConfiguration().getFinalizerName()); } @@ -497,7 +888,10 @@ public static

P addFinalizerWithSSA(Context

context) * the target resource, setting only name, namespace and finalizer. Does not use optimistic * locking for the patch. * + * @param context of reconciler + * @param finalizerName name of the finalizer to add * @return the patched resource from the server response + * @param

primary resource type */ public static

P addFinalizerWithSSA( Context

context, String finalizerName) { @@ -516,7 +910,7 @@ public static

P addFinalizerWithSSA( resource.setMetadata(objectMeta); resource.addFinalizer(finalizerName); - return serverSideApplyPrimary(context, resource); + return serverSideApplyPrimary(context, resource, false); } catch (InstantiationException | IllegalAccessException | InvocationTargetException @@ -529,11 +923,32 @@ public static

P addFinalizerWithSSA( } } + /** + * Compares resource versions of two resources. This is a convenience method that extracts the + * resource versions from the metadata and delegates to {@link #compareResourceVersions(String, + * String)}. + * + * @param h1 first resource + * @param h2 second resource + * @return negative if h1 is older, zero if equal, positive if h1 is newer + * @throws NonComparableResourceVersionException if either resource version is invalid + */ public static int compareResourceVersions(HasMetadata h1, HasMetadata h2) { return compareResourceVersions( h1.getMetadata().getResourceVersion(), h2.getMetadata().getResourceVersion()); } + /** + * Compares two Kubernetes resource versions numerically. Kubernetes resource versions are + * expected to be numeric strings that increase monotonically. This method assumes both versions + * are valid numeric strings without leading zeros. + * + * @param v1 first resource version + * @param v2 second resource version + * @return negative if v1 is older, zero if equal, positive if v1 is newer + * @throws NonComparableResourceVersionException if either resource version is empty, has leading + * zeros, or contains non-numeric characters + */ public static int compareResourceVersions(String v1, String v2) { int v1Length = validateResourceVersion(v1); int v2Length = validateResourceVersion(v2); From a1f943cd4796ac529369691cca0764b4c2778fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Dec 2025 16:08:56 +0100 Subject: [PATCH 21/26] wip --- .../operator/processing/event/ReconciliationDispatcher.java | 4 +--- .../PatchResourceAndStatusNoSSAReconciler.java | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index c06093f271..556362c6c1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -377,7 +377,6 @@ static class CustomResourceFacade { private final MixedOperation, Resource> resourceOperation; private final boolean useSSA; - private final String fieldManager; private final Cloner cloner; public CustomResourceFacade( @@ -386,7 +385,6 @@ public CustomResourceFacade( Cloner cloner) { this.resourceOperation = resourceOperation; this.useSSA = configuration.getConfigurationService().useSSAToPatchPrimaryResource(); - this.fieldManager = configuration.fieldManager(); this.cloner = cloner; } @@ -441,7 +439,7 @@ private R editStatus(Context context, R resource, R originalResource) { resource.getMetadata().setResourceVersion(null); return ReconcileUtils.jsonPatchPrimaryStatus( context, - originalResource, + clonedOriginal, r -> { ReconcilerUtilsInternal.setStatus(r, ReconcilerUtilsInternal.getStatus(resource)); return r; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java index e091896597..eb19f9e249 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java @@ -45,7 +45,7 @@ public UpdateControl reconcile( Context context) { numberOfExecutions.addAndGet(1); - log.info("Value: " + resource.getSpec().getValue()); + log.info("Value: {}", resource.getSpec().getValue()); if (removeAnnotation) { resource.getMetadata().getAnnotations().remove(TEST_ANNOTATION); From 9ecb404af7422ea54fc7bf31094d92471f8e00af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Dec 2025 16:18:37 +0100 Subject: [PATCH 22/26] wip --- .../processing/event/source/informer/InformerEventSource.java | 1 - 1 file changed, 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index fa0dc9502d..8f14f4961e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -27,7 +27,6 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; From d36eb311e252582087b6259773f94fccc0a1cdf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sat, 1 Nov 2025 00:54:45 +0100 Subject: [PATCH 23/26] feat: comparable resource version utils --- .../api/reconciler/ReconcilerUtils.java | 124 ++++++++++++++++++ .../event/ReconciliationDispatcher.java | 5 +- 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java new file mode 100644 index 0000000000..48a1bb46b9 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java @@ -0,0 +1,124 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.util.function.UnaryOperator; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; + +public class ReconcilerUtils { + // toto namespace handling + // todo compare resource version if multiple event sources provide the same resource + // for json patch make sense to retry (json merge patch?) + + public static R ssa(Context context, R resource) { + return handleResourcePatch( + context, + resource, + r -> + context + .getClient() + .resource(r) + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build())); + } + + public static R ssaStatus( + Context context, R resource) { + return handleResourcePatch( + context, + resource, + r -> + context + .getClient() + .resource(r) + .subresource("status") + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build())); + } + + public static

P ssaPrimary(Context

context, P resource) { + return handleResourcePatch( + resource, + r -> + context + .getClient() + .resource(r) + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()), + true, + context.eventSourceRetriever().getControllerEventSource()); + } + + public static

P ssaStatusPrimary(Context

context, P resource) { + return handleResourcePatch( + resource, + r -> + context + .getClient() + .resource(r) + .subresource("status") + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()), + true, + context.eventSourceRetriever().getControllerEventSource()); + } + + public static R handleResourcePatch( + Context context, R resource, UnaryOperator updateOperation) { + var esList = context.eventSourceRetriever().getEventSourcesFor(resource.getClass()); + if (esList.isEmpty()) { + throw new IllegalStateException("No event source found for type: " + resource.getClass()); + } + if (esList.size() > 1) { + throw new IllegalStateException( + "Multiple event sources found for: " + + resource.getClass() + + " please provide the target event source"); + } + var es = esList.get(0); + if (es instanceof ManagedInformerEventSource mes) { + return handleResourcePatch(resource, updateOperation, true, mes); + } else { + throw new IllegalStateException( + "Target event source must be a subclass off " + + ManagedInformerEventSource.class.getName()); + } + } + + @SuppressWarnings("unchecked") + private static R handleResourcePatch( + R resource, + UnaryOperator updateOperation, + boolean doNotLock, + ManagedInformerEventSource ies) { + var resourceVersion = resource.getMetadata().getResourceVersion(); + try { + if (resourceVersion != null && doNotLock) { + resource.getMetadata().setResourceVersion(null); + } + return (R) ies.updateAndCacheResource(resource, updateOperation); + } finally { + if (resourceVersion != null && doNotLock) { + resource.getMetadata().setResourceVersion(resourceVersion); + } + } + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 556362c6c1..5b52f3f9b6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -35,7 +35,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils; +import io.javaoperatorsdk.operator.api.reconciler.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.Controller; @@ -420,7 +420,8 @@ public R patchStatus(Context context, R resource, R originalResource) { var managedFields = resource.getMetadata().getManagedFields(); try { resource.getMetadata().setManagedFields(null); - return ReconcileUtils.serverSideApplyPrimaryStatus(context, resource); + var res = resource(resource); + return ReconcilerUtils.ssaStatusPrimary(context, resource); } finally { resource.getMetadata().setManagedFields(managedFields); } From 84ea5d04d913acac48e0325175f142f8c0135397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 3 Dec 2025 16:05:22 +0100 Subject: [PATCH 24/26] merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/reconciler/ReconcilerUtils.java | 139 ++---------------- .../event/ReconciliationDispatcher.java | 5 +- 2 files changed, 17 insertions(+), 127 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java index 48a1bb46b9..3e1a4f9b14 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java @@ -1,124 +1,15 @@ -package io.javaoperatorsdk.operator.api.reconciler; - -import java.util.function.UnaryOperator; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.dsl.base.PatchContext; -import io.fabric8.kubernetes.client.dsl.base.PatchType; -import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; - -public class ReconcilerUtils { - // toto namespace handling - // todo compare resource version if multiple event sources provide the same resource - // for json patch make sense to retry (json merge patch?) - - public static R ssa(Context context, R resource) { - return handleResourcePatch( - context, - resource, - r -> - context - .getClient() - .resource(r) - .patch( - new PatchContext.Builder() - .withForce(true) - .withFieldManager(context.getControllerConfiguration().fieldManager()) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build())); - } - - public static R ssaStatus( - Context context, R resource) { - return handleResourcePatch( - context, - resource, - r -> - context - .getClient() - .resource(r) - .subresource("status") - .patch( - new PatchContext.Builder() - .withForce(true) - .withFieldManager(context.getControllerConfiguration().fieldManager()) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build())); - } - - public static

P ssaPrimary(Context

context, P resource) { - return handleResourcePatch( - resource, - r -> - context - .getClient() - .resource(r) - .patch( - new PatchContext.Builder() - .withForce(true) - .withFieldManager(context.getControllerConfiguration().fieldManager()) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build()), - true, - context.eventSourceRetriever().getControllerEventSource()); - } - - public static

P ssaStatusPrimary(Context

context, P resource) { - return handleResourcePatch( - resource, - r -> - context - .getClient() - .resource(r) - .subresource("status") - .patch( - new PatchContext.Builder() - .withForce(true) - .withFieldManager(context.getControllerConfiguration().fieldManager()) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build()), - true, - context.eventSourceRetriever().getControllerEventSource()); - } - - public static R handleResourcePatch( - Context context, R resource, UnaryOperator updateOperation) { - var esList = context.eventSourceRetriever().getEventSourcesFor(resource.getClass()); - if (esList.isEmpty()) { - throw new IllegalStateException("No event source found for type: " + resource.getClass()); - } - if (esList.size() > 1) { - throw new IllegalStateException( - "Multiple event sources found for: " - + resource.getClass() - + " please provide the target event source"); - } - var es = esList.get(0); - if (es instanceof ManagedInformerEventSource mes) { - return handleResourcePatch(resource, updateOperation, true, mes); - } else { - throw new IllegalStateException( - "Target event source must be a subclass off " - + ManagedInformerEventSource.class.getName()); - } - } - - @SuppressWarnings("unchecked") - private static R handleResourcePatch( - R resource, - UnaryOperator updateOperation, - boolean doNotLock, - ManagedInformerEventSource ies) { - var resourceVersion = resource.getMetadata().getResourceVersion(); - try { - if (resourceVersion != null && doNotLock) { - resource.getMetadata().setResourceVersion(null); - } - return (R) ies.updateAndCacheResource(resource, updateOperation); - } finally { - if (resourceVersion != null && doNotLock) { - resource.getMetadata().setResourceVersion(resourceVersion); - } - } - } -} +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 5b52f3f9b6..556362c6c1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -35,7 +35,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.Controller; @@ -420,8 +420,7 @@ public R patchStatus(Context context, R resource, R originalResource) { var managedFields = resource.getMetadata().getManagedFields(); try { resource.getMetadata().setManagedFields(null); - var res = resource(resource); - return ReconcilerUtils.ssaStatusPrimary(context, resource); + return ReconcileUtils.serverSideApplyPrimaryStatus(context, resource); } finally { resource.getMetadata().setManagedFields(managedFields); } From 50617df3c28f412e2ede1a162191bef955e16f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 3 Dec 2025 16:06:16 +0100 Subject: [PATCH 25/26] wip --- .../operator/processing/event/ReconciliationDispatcher.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 556362c6c1..f0ac07f38f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -44,9 +44,7 @@ /** Handles calls and results of a Reconciler and finalizer related logic */ class ReconciliationDispatcher

{ - - public static final int MAX_UPDATE_RETRY = 10; - + private static final Logger log = LoggerFactory.getLogger(ReconciliationDispatcher.class); private final Controller

controller; From 9e305d3cb26fc2f8814b8753ad68c2863a43c286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 3 Dec 2025 16:06:21 +0100 Subject: [PATCH 26/26] wip --- .../operator/processing/event/ReconciliationDispatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index f0ac07f38f..a9e542ee8c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -44,7 +44,7 @@ /** Handles calls and results of a Reconciler and finalizer related logic */ class ReconciliationDispatcher

{ - + private static final Logger log = LoggerFactory.getLogger(ReconciliationDispatcher.class); private final Controller

controller;