From cd90e7e78ec301e95a44eb02c23d9b535477d595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 26 Jun 2025 13:01:43 +0200 Subject: [PATCH 1/5] feat: expectations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/reconciler/CacheAware.java | 44 +++++++++++++++++++ .../operator/api/reconciler/Context.java | 18 +------- .../reconciler/expectation/Expectation.java | 12 +++++ .../expectation/ExpectationContext.java | 8 ++++ 4 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/CacheAware.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/Expectation.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationContext.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/CacheAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/CacheAware.java new file mode 100644 index 0000000000..4f670deca5 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/CacheAware.java @@ -0,0 +1,44 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; +import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +public interface CacheAware

{ + default Optional getSecondaryResource(Class expectedType) { + return getSecondaryResource(expectedType, null); + } + + Set getSecondaryResources(Class expectedType); + + default Stream getSecondaryResourcesAsStream(Class expectedType) { + return getSecondaryResources(expectedType).stream(); + } + + Optional getSecondaryResource(Class expectedType, String eventSourceName); + + ControllerConfiguration

getControllerConfiguration(); + + /** + * Retrieves the primary resource. + * + * @return the primary resource associated with the current reconciliation + */ + P getPrimaryResource(); + + /** + * Retrieves the primary resource cache. + * + * @return the {@link IndexerResourceCache} associated with the associated {@link Reconciler} for + * this context + */ + @SuppressWarnings("unused") + IndexedResourceCache

getPrimaryCache(); + + EventSourceRetriever

eventSourceRetriever(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index f47deb9734..ed3fdc7093 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -12,24 +12,10 @@ import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; -public interface Context

{ +public interface Context

extends CacheAware

{ Optional getRetryInfo(); - default Optional getSecondaryResource(Class expectedType) { - return getSecondaryResource(expectedType, null); - } - - Set getSecondaryResources(Class expectedType); - - default Stream getSecondaryResourcesAsStream(Class expectedType) { - return getSecondaryResources(expectedType).stream(); - } - - Optional getSecondaryResource(Class expectedType, String eventSourceName); - - ControllerConfiguration

getControllerConfiguration(); - /** * Retrieve the {@link ManagedWorkflowAndDependentResourceContext} used to interact with {@link * io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource}s and associated {@link @@ -39,8 +25,6 @@ default Stream getSecondaryResourcesAsStream(Class expectedType) { */ ManagedWorkflowAndDependentResourceContext managedWorkflowAndDependentResourceContext(); - EventSourceRetriever

eventSourceRetriever(); - KubernetesClient getClient(); /** ExecutorService initialized by framework for workflows. Used for workflow standalone mode. */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/Expectation.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/Expectation.java new file mode 100644 index 0000000000..9f48bb5111 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/Expectation.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.api.reconciler.expectation; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +import java.time.Duration; + +public interface Expectation

{ + + boolean isMet(P primary, ExpectationContext

context); + + Duration timeout(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationContext.java new file mode 100644 index 0000000000..a7ea430dff --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationContext.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.api.reconciler.expectation; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.CacheAware; + +public interface ExpectationContext

extends CacheAware

{ + +} From d2597e5e7adf9dae887fc98b75c4fb93031411d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 26 Jun 2025 13:29:59 +0200 Subject: [PATCH 2/5] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/reconciler/BaseControl.java | 21 ++++++- .../operator/api/reconciler/CacheAware.java | 56 +++++++++---------- .../operator/api/reconciler/Context.java | 4 -- .../api/reconciler/DeleteControl.java | 8 ++- .../reconciler/ErrorStatusUpdateControl.java | 2 +- .../api/reconciler/UpdateControl.java | 2 +- .../expectation/AbstractExpectation.java | 22 ++++++++ .../reconciler/expectation/Expectation.java | 6 +- .../expectation/ExpectationAdapter.java | 21 +++++++ .../expectation/ExpectationContext.java | 4 +- .../expectation/ExpectationResult.java | 14 +++++ .../expectation/ExpectationStatus.java | 6 ++ .../event/ReconciliationDispatcher.java | 2 +- 13 files changed, 123 insertions(+), 45 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/AbstractExpectation.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationAdapter.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationStatus.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java index a5cdb85257..15e705dd8c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java @@ -3,10 +3,17 @@ import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.function.BiPredicate; -public abstract class BaseControl> { +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.expectation.Expectation; +import io.javaoperatorsdk.operator.api.reconciler.expectation.ExpectationAdapter; +import io.javaoperatorsdk.operator.api.reconciler.expectation.ExpectationContext; + +public abstract class BaseControl, P extends HasMetadata> { private Long scheduleDelay = null; + private Expectation

expectation; public T rescheduleAfter(long delay) { rescheduleAfter(Duration.ofMillis(delay)); @@ -25,4 +32,16 @@ public T rescheduleAfter(long delay, TimeUnit timeUnit) { public Optional getScheduleDelay() { return Optional.ofNullable(scheduleDelay); } + + public void expect(Expectation

expectation) { + this.expectation = expectation; + } + + public void expect(BiPredicate> expectation, Duration timeout) { + this.expectation = new ExpectationAdapter<>(expectation, timeout); + } + + public Expectation

getExpectation() { + return expectation; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/CacheAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/CacheAware.java index 4f670deca5..b29732214f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/CacheAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/CacheAware.java @@ -1,44 +1,44 @@ package io.javaoperatorsdk.operator.api.reconciler; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Stream; - public interface CacheAware

{ - default Optional getSecondaryResource(Class expectedType) { - return getSecondaryResource(expectedType, null); - } + default Optional getSecondaryResource(Class expectedType) { + return getSecondaryResource(expectedType, null); + } - Set getSecondaryResources(Class expectedType); + Set getSecondaryResources(Class expectedType); - default Stream getSecondaryResourcesAsStream(Class expectedType) { - return getSecondaryResources(expectedType).stream(); - } + default Stream getSecondaryResourcesAsStream(Class expectedType) { + return getSecondaryResources(expectedType).stream(); + } - Optional getSecondaryResource(Class expectedType, String eventSourceName); + Optional getSecondaryResource(Class expectedType, String eventSourceName); - ControllerConfiguration

getControllerConfiguration(); + ControllerConfiguration

getControllerConfiguration(); - /** - * Retrieves the primary resource. - * - * @return the primary resource associated with the current reconciliation - */ - P getPrimaryResource(); + /** + * Retrieves the primary resource. + * + * @return the primary resource associated with the current reconciliation + */ + P getPrimaryResource(); - /** - * Retrieves the primary resource cache. - * - * @return the {@link IndexerResourceCache} associated with the associated {@link Reconciler} for - * this context - */ - @SuppressWarnings("unused") - IndexedResourceCache

getPrimaryCache(); + /** + * Retrieves the primary resource cache. + * + * @return the {@link IndexerResourceCache} associated with the associated {@link Reconciler} for + * this context + */ + @SuppressWarnings("unused") + IndexedResourceCache

getPrimaryCache(); - EventSourceRetriever

eventSourceRetriever(); + EventSourceRetriever

eventSourceRetriever(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index ed3fdc7093..862e672cfb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -1,15 +1,11 @@ package io.javaoperatorsdk.operator.api.reconciler; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ExecutorService; -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.managed.ManagedWorkflowAndDependentResourceContext; -import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; public interface Context

extends CacheAware

{ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java index 7160e70830..a7f9104d6d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java @@ -1,6 +1,8 @@ package io.javaoperatorsdk.operator.api.reconciler; -public class DeleteControl extends BaseControl { +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class DeleteControl

extends BaseControl, P> { private final boolean removeFinalizer; @@ -27,7 +29,7 @@ public static DeleteControl defaultDelete() { * * @return delete control that will not remove finalizer. */ - public static DeleteControl noFinalizerRemoval() { + public static DeleteControl noFinalizerRemoval() { return new DeleteControl(false); } @@ -36,7 +38,7 @@ public boolean isRemoveFinalizer() { } @Override - public DeleteControl rescheduleAfter(long delay) { + public DeleteControl

rescheduleAfter(long delay) { if (removeFinalizer) { throw new IllegalStateException("Cannot reschedule cleanup if removing finalizer"); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java index e9073d613c..9b21d60883 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java @@ -6,7 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; public class ErrorStatusUpdateControl

- extends BaseControl> { + extends BaseControl, P> { private final P resource; private boolean noRetry = false; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java index 1b5eefd7ff..f3ebeb76d9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java @@ -5,7 +5,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; -public class UpdateControl

extends BaseControl> { +public class UpdateControl

extends BaseControl, P> { private final P resource; private final boolean patchResource; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/AbstractExpectation.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/AbstractExpectation.java new file mode 100644 index 0000000000..4821df4236 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/AbstractExpectation.java @@ -0,0 +1,22 @@ +package io.javaoperatorsdk.operator.api.reconciler.expectation; + +import java.time.Duration; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public abstract class AbstractExpectation

implements Expectation

{ + + protected final Duration timeout; + + protected AbstractExpectation(Duration timeout) { + this.timeout = timeout; + } + + @Override + public abstract boolean isFulfilled(P primary, ExpectationContext

context); + + @Override + public Duration timeout() { + return timeout; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/Expectation.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/Expectation.java index 9f48bb5111..47a97bd188 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/Expectation.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/Expectation.java @@ -1,12 +1,12 @@ package io.javaoperatorsdk.operator.api.reconciler.expectation; -import io.fabric8.kubernetes.api.model.HasMetadata; - import java.time.Duration; +import io.fabric8.kubernetes.api.model.HasMetadata; + public interface Expectation

{ - boolean isMet(P primary, ExpectationContext

context); + boolean isFulfilled(P primary, ExpectationContext

context); Duration timeout(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationAdapter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationAdapter.java new file mode 100644 index 0000000000..ca0707fc31 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationAdapter.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.api.reconciler.expectation; + +import java.time.Duration; +import java.util.function.BiPredicate; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class ExpectationAdapter

extends AbstractExpectation

{ + + private final BiPredicate> expectation; + + public ExpectationAdapter(BiPredicate> expectation, Duration timeout) { + super(timeout); + this.expectation = expectation; + } + + @Override + public boolean isFulfilled(P primary, ExpectationContext

context) { + return expectation.test(primary, context); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationContext.java index a7ea430dff..614a58dd41 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationContext.java @@ -3,6 +3,4 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.CacheAware; -public interface ExpectationContext

extends CacheAware

{ - -} +public interface ExpectationContext

extends CacheAware

{} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java new file mode 100644 index 0000000000..c7e5e2b4ff --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.api.reconciler.expectation; + +public class ExpectationResult { + + private ExpectationStatus status; + + public ExpectationResult(ExpectationStatus status) { + this.status = status; + } + + public ExpectationStatus getStatus() { + return status; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationStatus.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationStatus.java new file mode 100644 index 0000000000..5b9705520e --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationStatus.java @@ -0,0 +1,6 @@ +package io.javaoperatorsdk.operator.api.reconciler.expectation; + +public enum ExpectationStatus { + TIMEOUT, + FULFILLED; +} 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 c4b161ef27..f1bd59f2a2 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 @@ -250,7 +250,7 @@ private PostExecutionControl

createPostExecutionControl( } private void updatePostExecutionControlWithReschedule( - PostExecutionControl

postExecutionControl, BaseControl baseControl) { + PostExecutionControl

postExecutionControl, BaseControl baseControl) { baseControl.getScheduleDelay().ifPresent(postExecutionControl::withReSchedule); } From 181605b969edb04e4e86cc7d9cf28e1ffe6259de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 26 Jun 2025 13:32:32 +0200 Subject: [PATCH 3/5] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/reconciler/Context.java | 3 +++ .../operator/api/reconciler/DefaultContext.java | 6 ++++++ .../reconciler/expectation/ExpectationResult.java | 14 +++++++------- .../reconciler/expectation/ExpectationStatus.java | 4 ++-- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 862e672cfb..a2a45a2231 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext; +import io.javaoperatorsdk.operator.api.reconciler.expectation.ExpectationResult; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; public interface Context

extends CacheAware

{ @@ -52,4 +53,6 @@ public interface Context

extends CacheAware

{ * @return {@code true} is another reconciliation is already scheduled, {@code false} otherwise */ boolean isNextReconciliationImminent(); + + Optional expectationResult(); } 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 2acf8d13ca..e1ae63d351 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 @@ -11,6 +11,7 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedWorkflowAndDependentResourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext; +import io.javaoperatorsdk.operator.api.reconciler.expectation.ExpectationResult; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; import io.javaoperatorsdk.operator.processing.event.NoEventSourceForClassException; @@ -119,6 +120,11 @@ public boolean isNextReconciliationImminent() { .isNextReconciliationImminent(ResourceID.fromResource(primaryResource)); } + @Override + public Optional expectationResult() { + return Optional.empty(); + } + public DefaultContext

setRetryInfo(RetryInfo retryInfo) { this.retryInfo = retryInfo; return this; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java index c7e5e2b4ff..d501252c6c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java @@ -2,13 +2,13 @@ public class ExpectationResult { - private ExpectationStatus status; + private ExpectationStatus status; - public ExpectationResult(ExpectationStatus status) { - this.status = status; - } + public ExpectationResult(ExpectationStatus status) { + this.status = status; + } - public ExpectationStatus getStatus() { - return status; - } + public ExpectationStatus getStatus() { + return status; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationStatus.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationStatus.java index 5b9705520e..5ced5093da 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationStatus.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationStatus.java @@ -1,6 +1,6 @@ package io.javaoperatorsdk.operator.api.reconciler.expectation; public enum ExpectationStatus { - TIMEOUT, - FULFILLED; + TIMEOUT, + FULFILLED; } From 0856520884d1ca34907ebde70e0ffa248d652c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 27 Jun 2025 14:43:03 +0200 Subject: [PATCH 4/5] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/reconciler/Context.java | 2 +- .../operator/api/reconciler/DefaultContext.java | 2 +- .../api/reconciler/expectation/ExpectationResult.java | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index a2a45a2231..ddc30224a0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -54,5 +54,5 @@ public interface Context

extends CacheAware

{ */ boolean isNextReconciliationImminent(); - Optional expectationResult(); + Optional> expectationResult(); } 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 e1ae63d351..2a90f7de18 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 @@ -121,7 +121,7 @@ public boolean isNextReconciliationImminent() { } @Override - public Optional expectationResult() { + public Optional> expectationResult() { return Optional.empty(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java index d501252c6c..d8908eacd1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/ExpectationResult.java @@ -1,9 +1,13 @@ package io.javaoperatorsdk.operator.api.reconciler.expectation; -public class ExpectationResult { +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class ExpectationResult

{ private ExpectationStatus status; + private Expectation

expectation; + public ExpectationResult(ExpectationStatus status) { this.status = status; } @@ -11,4 +15,8 @@ public ExpectationResult(ExpectationStatus status) { public ExpectationStatus getStatus() { return status; } + + public Expectation

getExpectation() { + return expectation; + } } From 5363ec6f61f558239c86a786810007d607fd0803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 7 Jul 2025 08:53:13 +0200 Subject: [PATCH 5/5] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/reconciler/BaseControl.java | 4 +- .../api/reconciler/DefaultCacheAware.java | 79 +++++++++++++++++ .../api/reconciler/DefaultContext.java | 84 +++---------------- .../DefaultExpectationContext.java | 12 +++ .../processing/event/EventProcessor.java | 28 +++++++ .../processing/event/ExpectationHolder.java | 37 ++++++++ .../event/PostExecutionControl.java | 11 +++ .../event/ReconciliationDispatcher.java | 12 ++- .../processing/event/ResourceState.java | 17 ++++ .../api/reconciler/DefaultContextTest.java | 3 +- .../operator/processing/ControllerTest.java | 2 +- 11 files changed, 212 insertions(+), 77 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultCacheAware.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/DefaultExpectationContext.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExpectationHolder.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java index 15e705dd8c..565f2b4d9d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java @@ -41,7 +41,7 @@ public void expect(BiPredicate> expectation, Duration t this.expectation = new ExpectationAdapter<>(expectation, timeout); } - public Expectation

getExpectation() { - return expectation; + public Optional> getExpectation() { + return Optional.ofNullable(expectation); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultCacheAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultCacheAware.java new file mode 100644 index 0000000000..aad14bd860 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultCacheAware.java @@ -0,0 +1,79 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.Controller; +import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; +import io.javaoperatorsdk.operator.processing.event.NoEventSourceForClassException; + +public class DefaultCacheAware

implements CacheAware

{ + + protected final Controller

controller; + protected final P primaryResource; + + public DefaultCacheAware(Controller

controller, P primaryResource) { + this.controller = controller; + this.primaryResource = primaryResource; + } + + @Override + public Set getSecondaryResources(Class expectedType) { + return getSecondaryResourcesAsStream(expectedType).collect(Collectors.toSet()); + } + + @Override + public Stream getSecondaryResourcesAsStream(Class expectedType) { + return controller.getEventSourceManager().getEventSourcesFor(expectedType).stream() + .map(es -> es.getSecondaryResources(primaryResource)) + .flatMap(Set::stream); + } + + @Override + public Optional getSecondaryResource(Class expectedType, String eventSourceName) { + try { + return controller + .getEventSourceManager() + .getEventSourceFor(expectedType, eventSourceName) + .getSecondaryResource(primaryResource); + } catch (NoEventSourceForClassException e) { + /* + * If a workflow has an activation condition there can be event sources which are only + * registered if the activation condition holds, but to provide a consistent API we return an + * Optional instead of throwing an exception. + * + * Note that not only the resource which has an activation condition might not be registered + * but dependents which depend on it. + */ + if (eventSourceName == null && controller.workflowContainsDependentForType(expectedType)) { + return Optional.empty(); + } else { + throw e; + } + } + } + + @Override + public EventSourceRetriever

eventSourceRetriever() { + return controller.getEventSourceManager(); + } + + @Override + public ControllerConfiguration

getControllerConfiguration() { + return controller.getConfiguration(); + } + + @Override + public P getPrimaryResource() { + return primaryResource; + } + + @Override + public IndexedResourceCache

getPrimaryCache() { + return controller.getEventSourceManager().getControllerEventSource(); + } +} 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 2a90f7de18..190774767b 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 @@ -1,38 +1,36 @@ package io.javaoperatorsdk.operator.api.reconciler; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ExecutorService; -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.managed.DefaultManagedWorkflowAndDependentResourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext; import io.javaoperatorsdk.operator.api.reconciler.expectation.ExpectationResult; import io.javaoperatorsdk.operator.processing.Controller; -import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; -import io.javaoperatorsdk.operator.processing.event.NoEventSourceForClassException; import io.javaoperatorsdk.operator.processing.event.ResourceID; -public class DefaultContext

implements Context

{ +public class DefaultContext

extends DefaultCacheAware

+ implements Context

{ private RetryInfo retryInfo; - private final Controller

controller; - private final P primaryResource; - private final ControllerConfiguration

controllerConfiguration; + private final DefaultManagedWorkflowAndDependentResourceContext

defaultManagedDependentResourceContext; - public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { + private final ExpectationResult

expectationResult; + + public DefaultContext( + RetryInfo retryInfo, + Controller

controller, + P primaryResource, + ExpectationResult

expectationResult) { + super(controller, primaryResource); this.retryInfo = retryInfo; - this.controller = controller; - this.primaryResource = primaryResource; - this.controllerConfiguration = controller.getConfiguration(); this.defaultManagedDependentResourceContext = new DefaultManagedWorkflowAndDependentResourceContext<>(controller, primaryResource, this); + this.expectationResult = expectationResult; } @Override @@ -40,57 +38,11 @@ public Optional getRetryInfo() { return Optional.ofNullable(retryInfo); } - @Override - public Set getSecondaryResources(Class expectedType) { - return getSecondaryResourcesAsStream(expectedType).collect(Collectors.toSet()); - } - - @Override - public Stream getSecondaryResourcesAsStream(Class expectedType) { - return controller.getEventSourceManager().getEventSourcesFor(expectedType).stream() - .map(es -> es.getSecondaryResources(primaryResource)) - .flatMap(Set::stream); - } - - @Override - public Optional getSecondaryResource(Class expectedType, String eventSourceName) { - try { - return controller - .getEventSourceManager() - .getEventSourceFor(expectedType, eventSourceName) - .getSecondaryResource(primaryResource); - } catch (NoEventSourceForClassException e) { - /* - * If a workflow has an activation condition there can be event sources which are only - * registered if the activation condition holds, but to provide a consistent API we return an - * Optional instead of throwing an exception. - * - * Note that not only the resource which has an activation condition might not be registered - * but dependents which depend on it. - */ - if (eventSourceName == null && controller.workflowContainsDependentForType(expectedType)) { - return Optional.empty(); - } else { - throw e; - } - } - } - - @Override - public ControllerConfiguration

getControllerConfiguration() { - return controllerConfiguration; - } - @Override public ManagedWorkflowAndDependentResourceContext managedWorkflowAndDependentResourceContext() { return defaultManagedDependentResourceContext; } - @Override - public EventSourceRetriever

eventSourceRetriever() { - return controller.getEventSourceManager(); - } - @Override public KubernetesClient getClient() { return controller.getClient(); @@ -103,16 +55,6 @@ public ExecutorService getWorkflowExecutorService() { return controller.getExecutorServiceManager().workflowExecutorService(); } - @Override - public P getPrimaryResource() { - return primaryResource; - } - - @Override - public IndexedResourceCache

getPrimaryCache() { - return controller.getEventSourceManager().getControllerEventSource(); - } - @Override public boolean isNextReconciliationImminent() { return controller @@ -122,7 +64,7 @@ public boolean isNextReconciliationImminent() { @Override public Optional> expectationResult() { - return Optional.empty(); + return Optional.ofNullable(expectationResult); } public DefaultContext

setRetryInfo(RetryInfo retryInfo) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/DefaultExpectationContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/DefaultExpectationContext.java new file mode 100644 index 0000000000..7c08faea48 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/expectation/DefaultExpectationContext.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.api.reconciler.expectation; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.DefaultCacheAware; +import io.javaoperatorsdk.operator.processing.Controller; + +public class DefaultExpectationContext

extends DefaultCacheAware

+ implements ExpectationContext

{ + public DefaultExpectationContext(Controller

controller, P primaryResource) { + super(controller, primaryResource); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index bdaf575814..4dda3ae467 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -17,6 +17,8 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.expectation.DefaultExpectationContext; +import io.javaoperatorsdk.operator.api.reconciler.expectation.ExpectationContext; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; @@ -136,6 +138,10 @@ private void submitReconciliationExecution(ResourceState state) { Optional

maybeLatest = cache.get(resourceID); maybeLatest.ifPresent(MDCUtils::addResourceInfo); if (!controllerUnderExecution && maybeLatest.isPresent()) { + if (!shouldProceedWithExpectation(state, maybeLatest.orElseThrow())) { + return; + } + var rateLimit = state.getRateLimit(); if (rateLimit == null) { rateLimit = rateLimiter.initState(); @@ -174,6 +180,21 @@ private void submitReconciliationExecution(ResourceState state) { } } + boolean shouldProceedWithExpectation(ResourceState state, P primary) { + var optionalHolder = state.getExpectationHolder(); + if (optionalHolder.isEmpty()) { + return true; + } + var holder = optionalHolder.orElseThrow(); + if (holder.isTimedOut()) { + return true; + } + // todo cleanup state etc + ExpectationContext

expectationContext = + new DefaultExpectationContext<>(this.eventSourceManager.getController(), primary); + return holder.getExpectation().isFulfilled(primary, expectationContext); + } + private void handleEventMarking(Event event, ResourceState state) { final var relatedCustomResourceID = event.getRelatedCustomResourceID(); if (event instanceof ResourceEvent resourceEvent) { @@ -257,6 +278,9 @@ synchronized void eventProcessingFinished( state.markProcessedMarkForDeletion(); metrics.cleanupDoneFor(resourceID, metricsMetadata); } else { + // TODO what should be the relation between re-schedule and expectation + // should we add a flag if trigger if expectation fails + setExpectation(state, postExecutionControl); if (state.eventPresent()) { submitReconciliationExecution(state); } else { @@ -265,6 +289,10 @@ synchronized void eventProcessingFinished( } } + private void setExpectation(ResourceState state, PostExecutionControl

postExecutionControl) { + postExecutionControl.getExpectation().ifPresent(state::setExpectation); + } + /** * In case retry is configured more complex error logging takes place, see handleRetryOnException */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExpectationHolder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExpectationHolder.java new file mode 100644 index 0000000000..a50ab32615 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExpectationHolder.java @@ -0,0 +1,37 @@ +package io.javaoperatorsdk.operator.processing.event; + +import java.time.LocalDateTime; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.expectation.Expectation; + +public class ExpectationHolder

{ + + private LocalDateTime expectationCreationTime; + private Expectation

expectation; + + public ExpectationHolder(LocalDateTime expectationCreationTime, Expectation

expectation) { + this.expectationCreationTime = expectationCreationTime; + this.expectation = expectation; + } + + public LocalDateTime getExpectationCreationTime() { + return expectationCreationTime; + } + + public void setExpectationCreationTime(LocalDateTime expectationCreationTime) { + this.expectationCreationTime = expectationCreationTime; + } + + public Expectation getExpectation() { + return expectation; + } + + public void setExpectation(Expectation

expectation) { + this.expectation = expectation; + } + + public boolean isTimedOut() { + return expectationCreationTime.plus(expectation.timeout()).isBefore(LocalDateTime.now()); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java index 42311c1cb5..3841a80960 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java @@ -3,6 +3,7 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.expectation.Expectation; final class PostExecutionControl { @@ -10,6 +11,7 @@ final class PostExecutionControl { private final R updatedCustomResource; private final boolean updateIsStatusPatch; private final Exception runtimeException; + private Expectation expectation; private Long reScheduleDelay = null; @@ -66,6 +68,11 @@ public PostExecutionControl withReSchedule(long delay) { return this; } + public PostExecutionControl withExpectation(Expectation expectation) { + this.expectation = expectation; + return this; + } + public Optional getRuntimeException() { return Optional.ofNullable(runtimeException); } @@ -93,4 +100,8 @@ public String toString() { public boolean isFinalizerRemoved() { return finalizerRemoved; } + + public Optional> getExpectation() { + return Optional.ofNullable(expectation); + } } 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 f1bd59f2a2..16f2b03942 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 @@ -90,7 +90,7 @@ private PostExecutionControl

handleDispatch(ExecutionScope

executionScope) } Context

context = - new DefaultContext<>(executionScope.getRetryInfo(), controller, resourceForExecution); + new DefaultContext<>(executionScope.getRetryInfo(), controller, resourceForExecution, null); if (markedForDeletion) { return handleCleanup(resourceForExecution, originalResource, context); } else { @@ -246,9 +246,16 @@ private PostExecutionControl

createPostExecutionControl( postExecutionControl = PostExecutionControl.defaultDispatch(); } updatePostExecutionControlWithReschedule(postExecutionControl, updateControl); + updatePostExecutionControlWithExpectation(postExecutionControl, updateControl); + return postExecutionControl; } + private void updatePostExecutionControlWithExpectation( + PostExecutionControl

postExecutionControl, BaseControl baseControl) { + baseControl.getExpectation().ifPresent(postExecutionControl::withExpectation); + } + private void updatePostExecutionControlWithReschedule( PostExecutionControl

postExecutionControl, BaseControl baseControl) { baseControl.getScheduleDelay().ifPresent(postExecutionControl::withReSchedule); @@ -262,7 +269,7 @@ private PostExecutionControl

handleCleanup( ResourceID.fromResource(resourceForExecution), getVersion(resourceForExecution)); } - DeleteControl deleteControl = controller.cleanup(resourceForExecution, context); + DeleteControl

deleteControl = controller.cleanup(resourceForExecution, context); final var useFinalizer = controller.useFinalizer(); if (useFinalizer) { // note that we don't reschedule here even if instructed. Removing finalizer means that @@ -299,6 +306,7 @@ private PostExecutionControl

handleCleanup( useFinalizer); PostExecutionControl

postExecutionControl = PostExecutionControl.defaultDispatch(); updatePostExecutionControlWithReschedule(postExecutionControl, deleteControl); + updatePostExecutionControlWithExpectation(postExecutionControl, deleteControl); return postExecutionControl; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java index 5d4e74d681..bad045b261 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceState.java @@ -1,5 +1,9 @@ package io.javaoperatorsdk.operator.processing.event; +import java.time.LocalDateTime; +import java.util.Optional; + +import io.javaoperatorsdk.operator.api.reconciler.expectation.Expectation; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter.RateLimitState; import io.javaoperatorsdk.operator.processing.retry.RetryExecution; @@ -29,6 +33,7 @@ private enum EventingState { private RetryExecution retry; private EventingState eventing; private RateLimitState rateLimit; + private ExpectationHolder expectationHolder; public ResourceState(ResourceID id) { this.id = id; @@ -75,6 +80,18 @@ public boolean processedMarkForDeletionPresent() { return eventing == EventingState.PROCESSED_MARK_FOR_DELETION; } + public void setExpectation(Expectation expectation) { + expectationHolder = new ExpectationHolder(LocalDateTime.now(), expectation); + } + + public void cleanExpectation() { + expectationHolder = null; + } + + public Optional getExpectationHolder() { + return Optional.ofNullable(expectationHolder); + } + public void markEventReceived() { if (deleteEventPresent()) { throw new IllegalStateException("Cannot receive event after a delete event received"); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java index b289d68b22..de3e1b4221 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContextTest.java @@ -18,7 +18,8 @@ class DefaultContextTest { private final Secret primary = new Secret(); private final Controller mockController = mock(); - private final DefaultContext context = new DefaultContext<>(null, mockController, primary); + private final DefaultContext context = + new DefaultContext<>(null, mockController, primary, null); @Test @SuppressWarnings("unchecked") diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index 82ecdb111a..8b57506e26 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -122,7 +122,7 @@ void callsCleanupOnWorkflowWhenHasCleanerAndReconcilerIsNotCleaner( new Controller( reconciler, configuration, MockKubernetesClient.client(Secret.class)); - controller.cleanup(new Secret(), new DefaultContext<>(null, controller, new Secret())); + controller.cleanup(new Secret(), new DefaultContext<>(null, controller, new Secret(), null)); verify(managedWorkflowMock, times(workflowCleanerExecuted ? 1 : 0)).cleanup(any(), any()); }