diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/MockController.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/MockController.java
new file mode 100644
index 0000000000..d9a5250860
--- /dev/null
+++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/MockController.java
@@ -0,0 +1,20 @@
+package io.javaoperatorsdk.operator.processing;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.javaoperatorsdk.operator.MockKubernetesClient;
+import io.javaoperatorsdk.operator.api.config.BaseConfigurationService;
+import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class MockController {
+ public static
Controller
mockController(Class
primaryClass) {
+ Reconciler
reconciler = mock(Reconciler.class);
+ final var conf = MockControllerConfiguration.forResource(primaryClass);
+ final var configurationService = new BaseConfigurationService();
+ when(conf.getConfigurationService()).thenReturn(configurationService);
+ return new Controller<>(reconciler, conf, MockKubernetesClient.client(primaryClass));
+ }
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnce.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnce.java
new file mode 100644
index 0000000000..e091a6a646
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnce.java
@@ -0,0 +1,18 @@
+package io.javaoperatorsdk.operator.dependent.desiredonce;
+
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Version;
+
+@Group("io.josdk")
+@Version("v1")
+public class DesiredOnce extends CustomResource {
+ static final String KEY = "key";
+ static final String VALUE = "value";
+
+ public DesiredOnce() {}
+
+ public DesiredOnce(String value) {
+ this.spec = new DesiredOnceSpec(value);
+ }
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceDependent.java
new file mode 100644
index 0000000000..c904b49658
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceDependent.java
@@ -0,0 +1,35 @@
+package io.javaoperatorsdk.operator.dependent.desiredonce;
+
+import java.util.Map;
+
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
+import io.javaoperatorsdk.operator.api.config.informer.Informer;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(informer = @Informer(labelSelector = "desiredonce=true"))
+public class DesiredOnceDependent extends CRUDKubernetesDependentResource {
+ private boolean desiredAlreadyCalled;
+
+ @Override
+ protected ConfigMap desired(DesiredOnce primary, Context context) {
+ if (desiredAlreadyCalled) {
+ throw new IllegalStateException("desired should have only been called once");
+ }
+ desiredAlreadyCalled = true;
+ return new ConfigMapBuilder()
+ .editOrNewMetadata()
+ .withName(getName(primary))
+ .withNamespace(primary.getMetadata().getNamespace())
+ .withLabels(Map.of("desiredonce", "true"))
+ .endMetadata()
+ .addToData(DesiredOnce.KEY, primary.getSpec().value())
+ .build();
+ }
+
+ static String getName(DesiredOnce primary) {
+ return primary.getMetadata().getName() + "cm";
+ }
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceIT.java
new file mode 100644
index 0000000000..658bf4184f
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceIT.java
@@ -0,0 +1,38 @@
+package io.javaoperatorsdk.operator.dependent.desiredonce;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+class DesiredOnceIT {
+
+ @RegisterExtension
+ LocallyRunOperatorExtension operator =
+ LocallyRunOperatorExtension.builder().withReconciler(DesiredOnceReconciler.class).build();
+
+ @Test
+ public void checkThatDesiredIsOnlyCalledOncePerReconciliation() {
+ var resource = operator.create(testResource());
+
+ await()
+ .untilAsserted(
+ () -> {
+ var cm = operator.get(ConfigMap.class, DesiredOnceDependent.getName(resource));
+ assertThat(cm).isNotNull();
+ assertThat(cm.getData().get(DesiredOnce.KEY)).isEqualTo(DesiredOnce.VALUE);
+ });
+ }
+
+ private DesiredOnce testResource() {
+ var res = new DesiredOnce(DesiredOnce.VALUE);
+ res.setMetadata(
+ new ObjectMetaBuilder().withName("test").withNamespace(operator.getNamespace()).build());
+ return res;
+ }
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceReconciler.java
new file mode 100644
index 0000000000..5e7844322b
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceReconciler.java
@@ -0,0 +1,16 @@
+package io.javaoperatorsdk.operator.dependent.desiredonce;
+
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.Workflow;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+
+@Workflow(dependents = @Dependent(type = DesiredOnceDependent.class))
+public class DesiredOnceReconciler implements Reconciler {
+ @Override
+ public UpdateControl reconcile(DesiredOnce resource, Context context)
+ throws Exception {
+ return UpdateControl.noUpdate();
+ }
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceSpec.java
new file mode 100644
index 0000000000..ae31b526b2
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/desiredonce/DesiredOnceSpec.java
@@ -0,0 +1,3 @@
+package io.javaoperatorsdk.operator.dependent.desiredonce;
+
+public record DesiredOnceSpec(String value) {}