diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 8edfec98e5..bcd657eca3 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -160,9 +160,7 @@ larger than the `.observedGeneration` field on status. In order to have this fea . If these conditions are fulfilled and generation awareness not turned off, the observed generation is automatically set -by the framework after the `reconcile` method is called. There is just one exception, when the reconciler returns -with `UpdateControl.updateResource`, in this case the status is not updated, just the custom resource - however, this -update will lead to a new reconciliation. Note that the observed generation is updated also +by the framework after the `reconcile` method is called. Note that the observed generation is updated also when `UpdateControl.noUpdate()` is returned from the reconciler. See this feature working in the [WebPage example](https://github.com/java-operator-sdk/java-operator-sdk/blob/b91221bb54af19761a617bf18eef381e8ceb3b4c/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java#L5) . @@ -223,6 +221,30 @@ public class DeploymentReconciler } ``` +## Max Interval Between Reconciliations + +In case informers are all in place and reconciler is implemented correctly, there is no need for additional triggers. +However, it's a [common practice](https://github.com/java-operator-sdk/java-operator-sdk/issues/848#issuecomment-1016419966) +to have a failsafe periodic trigger in place, +just to make sure the resources are reconciled after certain time. This functionality is in place by default, there +is quite high interval (currently 10 hours) while the reconciliation is triggered. See how to override this using +the standard annotation: + +```java +@ControllerConfiguration(finalizerName = NO_FINALIZER, + reconciliationMaxInterval = @ReconciliationMaxInterval( + interval = 50, + timeUnit = TimeUnit.MILLISECONDS)) +``` + +The event is not propagated in a fixed rate, rather it's scheduled after each reconciliation. So the +next reconciliation will after at most within the specified interval after last reconciliation. + +This feature can be turned off by setting `reconciliationMaxInterval` to [`Constants.NO_RECONCILIATION_MAX_INTERVAL`](https://github.com/java-operator-sdk/java-operator-sdk/blob/442e7d8718e992a36880e42bd0a5c01affaec9df/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java#L8-L8) +or any non-positive number. + +The automatic retries are not affected by this feature, in case of an error no schedule is set by this feature. + ## Automatic Retries on Error When an exception is thrown from a controller, the framework will schedule an automatic retry of the reconciliation. The 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 b85df49eba..eb247752aa 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 @@ -1,7 +1,9 @@ package io.javaoperatorsdk.operator.api.config; import java.lang.reflect.ParameterizedType; +import java.time.Duration; import java.util.Collections; +import java.util.Optional; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -114,4 +116,8 @@ default boolean useFinalizer() { default ResourceEventFilter<R> getEventFilter() { return ResourceEventFilters.passthrough(); } + + default Optional<Duration> reconciliationMaxInterval() { + return Optional.of(Duration.ofHours(10L)); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index e8e2ef1162..c018c369f0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config; +import java.time.Duration; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -16,6 +17,7 @@ public class ControllerConfigurationOverrider<R extends HasMetadata> { private String labelSelector; private ResourceEventFilter<R> customResourcePredicate; private final ControllerConfiguration<R> original; + private Duration reconciliationMaxInterval; private ControllerConfigurationOverrider(ControllerConfiguration<R> original) { finalizer = original.getFinalizer(); @@ -24,7 +26,9 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) { retry = original.getRetryConfiguration(); labelSelector = original.getLabelSelector(); customResourcePredicate = original.getEventFilter(); + reconciliationMaxInterval = original.reconciliationMaxInterval().orElse(null); this.original = original; + } public ControllerConfigurationOverrider<R> withFinalizer(String finalizer) { @@ -74,6 +78,12 @@ public ControllerConfigurationOverrider<R> withCustomResourcePredicate( return this; } + public ControllerConfigurationOverrider<R> withReconciliationMaxInterval( + Duration reconciliationMaxInterval) { + this.reconciliationMaxInterval = reconciliationMaxInterval; + return this; + } + public ControllerConfiguration<R> build() { return new DefaultControllerConfiguration<>( original.getAssociatedReconcilerClassName(), @@ -86,6 +96,7 @@ public ControllerConfiguration<R> build() { labelSelector, customResourcePredicate, original.getResourceClass(), + reconciliationMaxInterval, original.getConfigurationService()); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java index 860152745b..33ff56899e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -1,6 +1,8 @@ package io.javaoperatorsdk.operator.api.config; +import java.time.Duration; import java.util.Collections; +import java.util.Optional; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -20,6 +22,7 @@ public class DefaultControllerConfiguration<R extends HasMetadata> private final String labelSelector; private final ResourceEventFilter<R> resourceEventFilter; private final Class<R> resourceClass; + private final Duration reconciliationMaxInterval; private ConfigurationService service; public DefaultControllerConfiguration( @@ -33,6 +36,7 @@ public DefaultControllerConfiguration( String labelSelector, ResourceEventFilter<R> resourceEventFilter, Class<R> resourceClass, + Duration reconciliationMaxInterval, ConfigurationService service) { this.associatedControllerClassName = associatedControllerClassName; this.name = name; @@ -41,6 +45,7 @@ public DefaultControllerConfiguration( this.generationAware = generationAware; this.namespaces = namespaces != null ? Collections.unmodifiableSet(namespaces) : Collections.emptySet(); + this.reconciliationMaxInterval = reconciliationMaxInterval; this.watchAllNamespaces = this.namespaces.isEmpty(); this.retryConfiguration = retryConfiguration == null @@ -122,4 +127,9 @@ public Class<R> getResourceClass() { public ResourceEventFilter<R> getEventFilter() { return resourceEventFilter; } + + @Override + public Optional<Duration> reconciliationMaxInterval() { + return Optional.ofNullable(reconciliationMaxInterval); + } } 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 075b4e79a3..85b3a00807 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 @@ -5,6 +5,7 @@ public final class Constants { public static final String EMPTY_STRING = ""; public static final String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT"; public static final String NO_FINALIZER = "JOSDK_NO_FINALIZER"; + public static final long NO_RECONCILIATION_MAX_INTERVAL = -1L; private Constants() {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index fad4bc8573..bd8863cad1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -4,6 +4,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @@ -56,4 +57,8 @@ */ @SuppressWarnings("rawtypes") Class<? extends ResourceEventFilter>[] eventFilters() default {}; + + ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMaxInterval( + interval = 10, timeUnit = TimeUnit.HOURS); + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconciliationMaxInterval.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconciliationMaxInterval.java new file mode 100644 index 0000000000..05459e7123 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconciliationMaxInterval.java @@ -0,0 +1,36 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface ReconciliationMaxInterval { + + /** + * A max delay between two reconciliations. Having this value larger than zero, will ensure that a + * reconciliation is scheduled with a target interval after the last reconciliation. Note that + * this not applies for retries, in case of an exception reconciliation is not scheduled. This is + * not a fixed rate, in other words a new reconciliation is scheduled after each reconciliation. + * <p/> + * If an interval is specified by {@link UpdateControl} or {@link DeleteControl}, those take + * precedence. + * <p/> + * This is a fail-safe feature, in the sense that if informers are in place and the reconciler + * implementation is correct, this feature can be turned off. + * <p/> + * Use NO_RECONCILIATION_MAX_INTERVAL in {@link Constants} to turn off this feature. + * + * @return max delay between reconciliations + **/ + long interval(); + + /** + * @return time unit for max delay between reconciliations + */ + TimeUnit timeUnit() default TimeUnit.HOURS; + +} 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 52374c5645..02c8f8cbb3 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 @@ -245,9 +245,12 @@ private PostExecutionControl<R> createPostExecutionControl(R updatedCustomResour private void updatePostExecutionControlWithReschedule( PostExecutionControl<R> postExecutionControl, BaseControl<?> baseControl) { - baseControl.getScheduleDelay().ifPresent(postExecutionControl::withReSchedule); + baseControl.getScheduleDelay().ifPresentOrElse(postExecutionControl::withReSchedule, + () -> controller.getConfiguration().reconciliationMaxInterval() + .ifPresent(m -> postExecutionControl.withReSchedule(m.toMillis()))); } + private PostExecutionControl<R> handleCleanup(R resource, Context context) { log.debug( "Executing delete for resource: {} with version: {}", diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java index a1cfa3e82d..19f65a9441 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java @@ -60,7 +60,8 @@ private static class TestControllerConfiguration<R extends HasMetadata> public TestControllerConfiguration(Reconciler<R> controller, Class<R> crClass) { super(null, getControllerName(controller), - CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, null); + CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, + null, null); this.controller = controller; } 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 9da51337d3..8e4be5e6dc 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 @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.event; +import java.time.Duration; import java.util.ArrayList; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -47,6 +48,7 @@ class ReconciliationDispatcherTest { private static final String DEFAULT_FINALIZER = "javaoperatorsdk.io/finalizer"; public static final String ERROR_MESSAGE = "ErrorMessage"; + public static final long RECONCILIATION_MAX_INTERVAL = 10L; private TestCustomResource testCustomResource; private ReconciliationDispatcher<TestCustomResource> reconciliationDispatcher; private final Reconciler<TestCustomResource> reconciler = mock(Reconciler.class, @@ -54,6 +56,7 @@ class ReconciliationDispatcherTest { private final ConfigurationService configService = mock(ConfigurationService.class); private final CustomResourceFacade<TestCustomResource> customResourceFacade = mock(ReconciliationDispatcher.CustomResourceFacade.class); + private ControllerConfiguration configuration = mock(ControllerConfiguration.class); @BeforeEach void setup() { @@ -63,17 +66,23 @@ void setup() { } private <R extends HasMetadata> ReconciliationDispatcher<R> init(R customResource, - Reconciler<R> reconciler, ControllerConfiguration<R> configuration, + Reconciler<R> reconciler, ControllerConfiguration configuration, CustomResourceFacade<R> customResourceFacade, boolean useFinalizer) { + configuration = configuration == null ? mock(ControllerConfiguration.class) : configuration; + ReconciliationDispatcherTest.this.configuration = configuration; final var finalizer = useFinalizer ? DEFAULT_FINALIZER : Constants.NO_FINALIZER; when(configuration.getFinalizer()).thenReturn(finalizer); when(configuration.useFinalizer()).thenCallRealMethod(); when(configuration.getName()).thenReturn("EventDispatcherTestController"); when(configuration.getResourceClass()).thenReturn((Class<R>) customResource.getClass()); when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT); + when(configuration.reconciliationMaxInterval()) + .thenReturn(Optional.of(Duration.ofHours(RECONCILIATION_MAX_INTERVAL))); + when(configuration.getConfigurationService()).thenReturn(configService); + /* * We need this for mock reconcilers to properly generate the expected UpdateControl: without * this, calls such as `when(reconciler.reconcile(eq(testCustomResource), @@ -429,6 +438,35 @@ void callErrorStatusHandlerEvenOnFirstError() { any(), any()); } + @Test + void schedulesReconciliationIfMaxDelayIsSet() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + + when(reconciler.reconcile(eq(testCustomResource), any())) + .thenReturn(UpdateControl.noUpdate()); + + PostExecutionControl control = + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + + assertThat(control.getReScheduleDelay()).isPresent() + .hasValue(TimeUnit.HOURS.toMillis(RECONCILIATION_MAX_INTERVAL)); + } + + @Test + void canSkipSchedulingMaxDelayIf() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + + when(reconciler.reconcile(eq(testCustomResource), any())) + .thenReturn(UpdateControl.noUpdate()); + when(configuration.reconciliationMaxInterval()) + .thenReturn(Optional.empty()); + + PostExecutionControl control = + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + + assertThat(control.getReScheduleDelay()).isNotPresent(); + } + private ObservedGenCustomResource createObservedGenCustomResource() { ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource(); observedGenCustomResource.setMetadata(new ObjectMeta()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java index 17168876a5..433614fe17 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java @@ -171,7 +171,7 @@ public ControllerConfig(String finalizer, boolean generationAware, null, eventFilter, customResourceClass, - null); + null, null); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index 5cdc85c553..5859115ee2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -170,6 +170,7 @@ public TestConfiguration(boolean generationAware) { null, null, TestCustomResource.class, + null, null); } } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 5941975a71..3393de7a55 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -112,6 +112,12 @@ public List<Reconciler> getReconcilers() { .collect(Collectors.toUnmodifiableList()); } + public Reconciler getFirstReconciler() { + return operator.getControllers().stream() + .map(Controller::getReconciler) + .findFirst().orElseThrow(); + } + @SuppressWarnings({"rawtypes"}) public <T extends Reconciler> T getControllerOfType(Class<T> type) { return operator.getControllers().stream() diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 1000cb61c4..8977bf3ff8 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.config.runtime; +import java.time.Duration; +import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -108,6 +110,21 @@ public ResourceEventFilter<R> getEventFilter() { : ResourceEventFilters.passthrough(); } + + + @Override + public Optional<Duration> reconciliationMaxInterval() { + if (annotation.reconciliationMaxInterval() != null) { + if (annotation.reconciliationMaxInterval().interval() <= 0) { + return Optional.empty(); + } + return Optional.of(Duration.of(annotation.reconciliationMaxInterval().interval(), + annotation.reconciliationMaxInterval().timeUnit().toChronoUnit())); + } else { + return io.javaoperatorsdk.operator.api.config.ControllerConfiguration.super.reconciliationMaxInterval(); + } + } + public static <T> T valueOrDefault(ControllerConfiguration controllerConfiguration, Function<ControllerConfiguration, T> mapper, T defaultValue) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java index 458e2d12eb..12cf3ca4d8 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class ConcurrencyIT { +class ConcurrencyIT { public static final int NUMBER_OF_RESOURCES_CREATED = 50; public static final int NUMBER_OF_RESOURCES_DELETED = 30; public static final int NUMBER_OF_RESOURCES_UPDATED = 20; @@ -34,7 +34,7 @@ public class ConcurrencyIT { .build(); @Test - public void manyResourcesGetCreatedUpdatedAndDeleted() throws InterruptedException { + void manyResourcesGetCreatedUpdatedAndDeleted() throws InterruptedException { log.info("Creating {} new resources", NUMBER_OF_RESOURCES_CREATED); for (int i = 0; i < NUMBER_OF_RESOURCES_CREATED; i++) { TestCustomResource tcr = TestUtils.testCustomResourceWithPrefix(String.valueOf(i)); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index b1f783b958..dbd015587e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -15,7 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class ControllerExecutionIT { +class ControllerExecutionIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() @@ -24,7 +24,7 @@ public class ControllerExecutionIT { .build(); @Test - public void configMapGetsCreatedForTestCustomResource() { + void configMapGetsCreatedForTestCustomResource() { operator.getControllerOfType(TestReconciler.class).setUpdateStatus(true); TestCustomResource resource = TestUtils.testCustomResource(); @@ -36,7 +36,7 @@ public void configMapGetsCreatedForTestCustomResource() { } @Test - public void eventIsSkippedChangedOnMetadataOnlyUpdate() { + void eventIsSkippedChangedOnMetadataOnlyUpdate() { operator.getControllerOfType(TestReconciler.class).setUpdateStatus(false); TestCustomResource resource = TestUtils.testCustomResource(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java index 876710ff36..70a3f04777 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java @@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat; -public class CustomResourceFilterIT { +class CustomResourceFilterIT { @RegisterExtension OperatorExtension operator = @@ -22,7 +22,7 @@ public class CustomResourceFilterIT { .build(); @Test - public void doesCustomFiltering() throws InterruptedException { + void doesCustomFiltering() throws InterruptedException { var filtered1 = createTestResource("filtered1", true, false); var filtered2 = createTestResource("filtered2", false, true); var notFiltered = createTestResource("notfiltered", true, true); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java index 4a1b3293e7..fbf54a1ad1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java @@ -15,7 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class ErrorStatusHandlerIT { +class ErrorStatusHandlerIT { public static final int MAX_RETRY_ATTEMPTS = 3; ErrorStatusHandlerTestReconciler reconciler = new ErrorStatusHandlerTestReconciler(); @@ -29,7 +29,7 @@ public class ErrorStatusHandlerIT { .build(); @Test - public void testErrorMessageSetEventually() { + void testErrorMessageSetEventually() { ErrorStatusHandlerTestCustomResource resource = operator.create(ErrorStatusHandlerTestCustomResource.class, createCustomResource()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index 033bbf2b8b..c91ebaad43 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -16,7 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class EventSourceIT { +class EventSourceIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() @@ -25,7 +25,7 @@ public class EventSourceIT { .build(); @Test - public void receivingPeriodicEvents() { + void receivingPeriodicEvents() { EventSourceTestCustomResource resource = createTestCustomResource("1"); operator.create(EventSourceTestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index 4885e7d356..7ff36bd375 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; -public class InformerEventSourceIT { +class InformerEventSourceIT { public static final String RESOURCE_NAME = "informertestcr"; public static final String INITIAL_STATUS_MESSAGE = "Initial Status"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java index 2c5ea1cba1..a2c7378ff5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class KubernetesResourceStatusUpdateIT { +class KubernetesResourceStatusUpdateIT { @RegisterExtension OperatorExtension operator = @@ -29,7 +29,7 @@ public class KubernetesResourceStatusUpdateIT { .build(); @Test - public void testReconciliationOfNonCustomResourceAndStatusUpdate() { + void testReconciliationOfNonCustomResourceAndStatusUpdate() { var deployment = operator.create(Deployment.class, testDeployment()); await().atMost(120, TimeUnit.SECONDS).untilAsserted(() -> { var d = operator.get(Deployment.class, deployment.getMetadata().getName()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java new file mode 100644 index 0000000000..f284dbf407 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java @@ -0,0 +1,45 @@ +package io.javaoperatorsdk.operator; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.maxinterval.MaxIntervalTestCustomResource; +import io.javaoperatorsdk.operator.sample.maxinterval.MaxIntervalTestReconciler; + +import static org.awaitility.Awaitility.await; + +class MaxIntervalIT { + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new MaxIntervalTestReconciler()) + .build(); + + @Test + void reconciliationTriggeredBasedOnMaxInterval() { + MaxIntervalTestCustomResource cr = createTestResource(); + + operator.create(MaxIntervalTestCustomResource.class, cr); + + await() + .pollInterval(50, TimeUnit.MILLISECONDS) + .atMost(500, TimeUnit.MILLISECONDS) + .until( + () -> ((MaxIntervalTestReconciler) operator.getFirstReconciler()) + .getNumberOfExecutions() > 3); + } + + private MaxIntervalTestCustomResource createTestResource() { + MaxIntervalTestCustomResource cr = new MaxIntervalTestCustomResource(); + cr.setMetadata(new ObjectMeta()); + cr.getMetadata().setName("maxintervaltest1"); + return cr; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java index 9eac198f35..5f86336a1c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java @@ -14,7 +14,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class ObservedGenerationHandlingIT { +class ObservedGenerationHandlingIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() @@ -23,7 +23,7 @@ public class ObservedGenerationHandlingIT { .build(); @Test - public void testReconciliationOfNonCustomResourceAndStatusUpdate() { + void testReconciliationOfNonCustomResourceAndStatusUpdate() { var resource = new ObservedGenerationTestCustomResource(); resource.setMetadata(new ObjectMeta()); resource.getMetadata().setName("observed-gen1"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index 4ab63c7513..c4ab025ac5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class RetryIT { +class RetryIT { public static final int RETRY_INTERVAL = 150; public static final int MAX_RETRY_ATTEMPTS = 5; @@ -36,7 +36,7 @@ public class RetryIT { @Test - public void retryFailedExecution() { + void retryFailedExecution() { RetryTestCustomResource resource = createTestCustomResource("1"); operator.create(RetryTestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java index e855b016fd..5ebb05eb0c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java @@ -12,7 +12,7 @@ import static io.javaoperatorsdk.operator.RetryIT.createTestCustomResource; import static org.assertj.core.api.Assertions.assertThat; -public class RetryMaxAttemptIT { +class RetryMaxAttemptIT { public static final int MAX_RETRY_ATTEMPTS = 3; public static final int RETRY_INTERVAL = 100; @@ -31,7 +31,7 @@ public class RetryMaxAttemptIT { @Test - public void retryFailedExecution() throws InterruptedException { + void retryFailedExecution() throws InterruptedException { RetryTestCustomResource resource = createTestCustomResource("max-retry"); operator.create(RetryTestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index 5998502f32..75597643bf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class SubResourceUpdateIT { +class SubResourceUpdateIT { @RegisterExtension OperatorExtension operator = @@ -28,7 +28,7 @@ public class SubResourceUpdateIT { .build(); @Test - public void updatesSubResourceStatus() { + void updatesSubResourceStatus() { SubResourceTestCustomResource resource = createTestCustomResource("1"); operator.create(SubResourceTestCustomResource.class, resource); @@ -41,7 +41,7 @@ public void updatesSubResourceStatus() { } @Test - public void updatesSubResourceStatusNoFinalizer() { + void updatesSubResourceStatusNoFinalizer() { SubResourceTestCustomResource resource = createTestCustomResource("1"); resource.getMetadata().setFinalizers(Collections.emptyList()); @@ -57,7 +57,7 @@ public void updatesSubResourceStatusNoFinalizer() { /** Note that we check on controller impl if there is finalizer on execution. */ @Test - public void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain() { + void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain() { SubResourceTestCustomResource resource = createTestCustomResource("1"); resource.getMetadata().getFinalizers().clear(); operator.create(SubResourceTestCustomResource.class, resource); @@ -77,7 +77,7 @@ public void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain * fail since its resource version is outdated already. */ @Test - public void updateCustomResourceAfterSubResourceChange() { + void updateCustomResourceAfterSubResourceChange() { SubResourceTestCustomResource resource = createTestCustomResource("1"); operator.create(SubResourceTestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java index 2a479d47df..6bad954400 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -17,7 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class UpdatingResAndSubResIT { +class UpdatingResAndSubResIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() @@ -26,7 +26,7 @@ public class UpdatingResAndSubResIT { .build(); @Test - public void updatesSubResourceStatus() { + void updatesSubResourceStatus() { DoubleUpdateTestCustomResource resource = createTestCustomResource("1"); operator.create(DoubleUpdateTestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResource.java new file mode 100644 index 0000000000..1c6cf81453 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.maxinterval; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("MaxIntervalTestCustomResource") +@ShortNames("mit") +public class MaxIntervalTestCustomResource + extends CustomResource<Void, MaxIntervalTestCustomResourceStatus> + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResourceStatus.java new file mode 100644 index 0000000000..5c403febfb --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResourceStatus.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.sample.maxinterval; + +public class MaxIntervalTestCustomResourceStatus { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java new file mode 100644 index 0000000000..a5343c27a4 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java @@ -0,0 +1,30 @@ +package io.javaoperatorsdk.operator.sample.maxinterval; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +@ControllerConfiguration(finalizerName = NO_FINALIZER, + reconciliationMaxInterval = @ReconciliationMaxInterval(interval = 50, + timeUnit = TimeUnit.MILLISECONDS)) +public class MaxIntervalTestReconciler + implements Reconciler<MaxIntervalTestCustomResource>, TestExecutionInfoProvider { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl<MaxIntervalTestCustomResource> reconcile( + MaxIntervalTestCustomResource resource, Context context) { + numberOfExecutions.addAndGet(1); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + +}