diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java new file mode 100644 index 0000000000..946fde48a3 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java @@ -0,0 +1,28 @@ +package io.javaoperatorsdk.operator.api; + +import java.util.Optional; + +import io.fabric8.kubernetes.client.CustomResource; + +/** + * If the custom resource's status implements this interface, the observed generation will be + * automatically handled. The last observed generation will be updated on status when the status is + * instructed to be updated (see below). In addition to that, controller configuration will be + * checked if is set to generation aware. If generation aware config is turned off, this interface + * is ignored. + * + * In order to work the status object returned by CustomResource.getStatus() should not be null. In + * addition to that from the controller that the + * {@link UpdateControl#updateStatusSubResource(CustomResource)} or + * {@link UpdateControl#updateCustomResourceAndStatus(CustomResource)} should be returned. The + * observed generation is not updated in other cases. + * + * @see ObservedGenerationAwareStatus + */ +public interface ObservedGenerationAware { + + void setObservedGeneration(Long generation); + + Optional getObservedGeneration(); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java new file mode 100644 index 0000000000..843736242b --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.api; + +import java.util.Optional; + +/** + * A helper base class for status sub-resources classes to extend to support generate awareness. + */ +public class ObservedGenerationAwareStatus implements ObservedGenerationAware { + + private Long observedGeneration; + + @Override + public void setObservedGeneration(Long generation) { + this.observedGeneration = generation; + } + + @Override + public Optional getObservedGeneration() { + return Optional.ofNullable(observedGeneration); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index dd8ae8dbae..3000e463b6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -117,11 +117,9 @@ private PostExecutionControl handleCreateOrUpdate( .getCustomResource() .getMetadata() .setResourceVersion(updatedCustomResource.getMetadata().getResourceVersion()); - updatedCustomResource = - customResourceFacade.updateStatus(updateControl.getCustomResource()); + updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource()); } else if (updateControl.isUpdateStatusSubResource()) { - updatedCustomResource = - customResourceFacade.updateStatus(updateControl.getCustomResource()); + updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource()); } else if (updateControl.isUpdateCustomResource()) { updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); } @@ -129,6 +127,22 @@ private PostExecutionControl handleCreateOrUpdate( } } + private R updateStatusGenerationAware(R customResource) { + updateStatusObservedGenerationIfRequired(customResource); + return customResourceFacade.updateStatus(customResource); + } + + private void updateStatusObservedGenerationIfRequired(R customResource) { + if (controller.getConfiguration().isGenerationAware()) { + var status = customResource.getStatus(); + // Note that if status is null we won't update the observed generation. + if (status instanceof ObservedGenerationAware) { + ((ObservedGenerationAware) status) + .setObservedGeneration(customResource.getMetadata().getGeneration()); + } + } + } + private PostExecutionControl createPostExecutionControl(R updatedCustomResource, UpdateControl updateControl) { PostExecutionControl postExecutionControl; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java index 2f3bc72327..7de41c455d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.processing.event.internal; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.ObservedGenerationAware; /** * Convenience implementations of, and utility methods for, {@link CustomResourceEventFilter}. @@ -21,9 +22,18 @@ public final class CustomResourceEventFilters { }; private static final CustomResourceEventFilter GENERATION_AWARE = - (configuration, oldResource, newResource) -> oldResource == null - || !configuration.isGenerationAware() - || oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration(); + (configuration, oldResource, newResource) -> { + final var status = newResource.getStatus(); + final var generationAware = configuration.isGenerationAware(); + if (generationAware && status instanceof ObservedGenerationAware) { + var actualGeneration = newResource.getMetadata().getGeneration(); + var observedGeneration = ((ObservedGenerationAware) status) + .getObservedGeneration(); + return observedGeneration.map(aLong -> actualGeneration > aLong).orElse(true); + } + return oldResource == null || !generationAware || + oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration(); + }; private static final CustomResourceEventFilter PASSTHROUGH = (configuration, oldResource, newResource) -> true; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java index da32b467e0..87385e61d2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java @@ -9,6 +9,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.Context; @@ -23,6 +24,7 @@ import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; +import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.ADDED; import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.UPDATED; @@ -320,6 +322,33 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); } + @Test + void setObservedGenerationForStatusIfNeeded() { + var observedGenResource = createObservedGenCustomResource(); + + when(configuration.isGenerationAware()).thenReturn(true); + when(controller.createOrUpdateResource(eq(observedGenResource), any())) + .thenReturn( + UpdateControl.updateStatusSubResource(observedGenResource)); + + when(customResourceFacade.updateStatus(observedGenResource)).thenReturn(observedGenResource); + + PostExecutionControl control = eventDispatcher.handleExecution( + executionScopeWithCREvent(ADDED, observedGenResource)); + assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration().get()) + .isEqualTo(1L); + } + + @Test + private ObservedGenCustomResource createObservedGenCustomResource() { + ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource(); + observedGenCustomResource.setMetadata(new ObjectMeta()); + observedGenCustomResource.getMetadata().setGeneration(1L); + observedGenCustomResource.getMetadata().setFinalizers(new ArrayList<>()); + observedGenCustomResource.getMetadata().getFinalizers().add(DEFAULT_FINALIZER); + return observedGenCustomResource; + } + private void markForDeletion(CustomResource customResource) { customResource.getMetadata().setDeletionTimestamp("2019-8-10"); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java index 3e9e413f1e..86081e4af1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java @@ -7,6 +7,8 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; @@ -15,6 +17,7 @@ import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.mockito.Mockito.any; @@ -94,6 +97,31 @@ public void eventFilteredByCustomPredicateAndGenerationAware() { verify(eventHandler, times(1)).handleEvent(any()); } + @Test + public void observedGenerationFiltering() { + var config = new ObservedGenControllerConfig(FINALIZER, true, null); + when(config.getConfigurationService().getResourceCloner()) + .thenReturn(ConfigurationService.DEFAULT_CLONER); + + var controller = new ObservedGenConfiguredController(config); + var eventSource = new CustomResourceEventSource<>(controller); + eventSource.setEventHandler(eventHandler); + + ObservedGenCustomResource cr = new ObservedGenCustomResource(); + cr.setMetadata(new ObjectMeta()); + cr.getMetadata().setFinalizers(List.of(FINALIZER)); + cr.getMetadata().setGeneration(5L); + cr.getStatus().setObservedGeneration(5L); + + eventSource.eventReceived(ResourceAction.UPDATED, cr, null); + verify(eventHandler, times(0)).handleEvent(any()); + + cr.getMetadata().setGeneration(6L); + + eventSource.eventReceived(ResourceAction.UPDATED, cr, null); + verify(eventHandler, times(1)).handleEvent(any()); + } + @Test public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { var config = new TestControllerConfig( @@ -124,11 +152,25 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { verify(eventHandler, times(2)).handleEvent(any()); } - private static class TestControllerConfig extends - DefaultControllerConfiguration { - + private static class TestControllerConfig extends ControllerConfig { public TestControllerConfig(String finalizer, boolean generationAware, CustomResourceEventFilter eventFilter) { + super(finalizer, generationAware, eventFilter, TestCustomResource.class); + } + } + private static class ObservedGenControllerConfig + extends ControllerConfig { + public ObservedGenControllerConfig(String finalizer, boolean generationAware, + CustomResourceEventFilter eventFilter) { + super(finalizer, generationAware, eventFilter, ObservedGenCustomResource.class); + } + } + + private static class ControllerConfig> extends + DefaultControllerConfiguration { + + public ControllerConfig(String finalizer, boolean generationAware, + CustomResourceEventFilter eventFilter, Class customResourceClass) { super( null, null, @@ -139,7 +181,7 @@ public TestControllerConfig(String finalizer, boolean generationAware, null, null, eventFilter, - TestCustomResource.class, + customResourceClass, mock(ConfigurationService.class)); when(getConfigurationService().getResourceCloner()) @@ -158,4 +200,18 @@ public MixedOperation { + + public ObservedGenConfiguredController( + ControllerConfiguration configuration) { + super(null, configuration, null); + } + + @Override + public MixedOperation, Resource> getCRClient() { + return mock(MixedOperation.class); + } + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenCustomResource.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenCustomResource.java new file mode 100644 index 0000000000..74f58b795f --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenCustomResource.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk.io") +@Version("v1") +public class ObservedGenCustomResource + extends CustomResource { + + @Override + protected ObservedGenSpec initSpec() { + return new ObservedGenSpec(); + } + + @Override + protected ObservedGenStatus initStatus() { + return new ObservedGenStatus(); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenSpec.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenSpec.java new file mode 100644 index 0000000000..181204bc1c --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenSpec.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +public class ObservedGenSpec { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String toString() { + return "TestCustomResourceSpec{" + + "value='" + value + '\'' + + '}'; + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenStatus.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenStatus.java new file mode 100644 index 0000000000..d4ffee5416 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenStatus.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus; + +public class ObservedGenStatus extends ObservedGenerationAwareStatus { + +}