From 9747c38d12de5eeb25c0cb562ecfec896b14607b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 2 Jul 2024 10:57:05 +0200 Subject: [PATCH] feat: configure max reconciliation interval via property Fixes #893 Signed-off-by: Chris Laprun --- .../runtime/ConfigurationServiceRecorder.java | 1 + .../QuarkusControllerConfiguration.java | 12 ++++++--- .../RunTimeControllerConfiguration.java | 13 ++++++++- .../pages/includes/quarkus-operator-sdk.adoc | 20 +++++++++++++- .../operatorsdk/it/SecretReconciler.java | 4 ++- .../src/main/resources/application.properties | 3 +++ .../it/OperatorSDKResourceTest.java | 27 +++++++++++++++++-- 7 files changed, 71 insertions(+), 9 deletions(-) diff --git a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/ConfigurationServiceRecorder.java b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/ConfigurationServiceRecorder.java index 254c44592..dc40a6785 100644 --- a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/ConfigurationServiceRecorder.java +++ b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/ConfigurationServiceRecorder.java @@ -44,6 +44,7 @@ public Supplier configurationServiceSupplier(Versio if (extConfig != null) { extConfig.finalizer.ifPresent(c::setFinalizer); extConfig.selector.ifPresent(c::setLabelSelector); + extConfig.maxReconciliationInterval.ifPresent(c::setMaxReconciliationInterval); c.setRetryConfiguration(RetryConfigurationResolver.resolve(extConfig.retry)); setNamespacesFromRuntime(c, extConfig.namespaces); } diff --git a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusControllerConfiguration.java b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusControllerConfiguration.java index d80947e69..de851c88e 100644 --- a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusControllerConfiguration.java +++ b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusControllerConfiguration.java @@ -34,6 +34,7 @@ @SuppressWarnings("rawtypes") public class QuarkusControllerConfiguration implements ControllerConfiguration, DependentResourceConfigurationProvider { + // we need to create this class because Quarkus cannot reference the default implementation that // JOSDK provides as it doesn't like lambdas at build time. The class also needs to be public // because otherwise Quarkus isn't able to access it… :( @@ -71,7 +72,6 @@ public DefaultRateLimiter(Duration refreshPeriod, int limitForPeriod) { private final Class resourceClass; private final Optional informerListLimit; private final ResourceEventFilter eventFilter; - private final Optional maxReconciliationInterval; private final Optional> onAddFilter; private final Optional> onUpdateFilter; private final Optional> genericFilter; @@ -83,6 +83,7 @@ public DefaultRateLimiter(Duration refreshPeriod, int limitForPeriod) { private Class retryClass; private Class rateLimiterConfigurationClass; private Class rateLimiterClass; + private Optional maxReconciliationInterval; private String finalizer; private Set namespaces; private boolean wereNamespacesSet; @@ -225,8 +226,7 @@ void setNamespaces(Collection namespaces) { // propagate namespace changes to the dependents' config if needed this.dependentsMetadata.forEach((name, spec) -> { final var config = spec.getDependentResourceConfig(); - if (config instanceof QuarkusKubernetesDependentResourceConfig) { - final var qConfig = (QuarkusKubernetesDependentResourceConfig) config; + if (config instanceof QuarkusKubernetesDependentResourceConfig qConfig) { qConfig.setNamespaces(this.namespaces); } }); @@ -262,7 +262,7 @@ public String getLabelSelector() { return labelSelector; } - public void setLabelSelector(String labelSelector) { + void setLabelSelector(String labelSelector) { this.labelSelector = labelSelector; } @@ -321,6 +321,10 @@ public Duration getMaxReconciliationInterval() { return maxReconciliationInterval.orElseThrow(); } + void setMaxReconciliationInterval(Duration duration) { + maxReconciliationInterval = Optional.of(duration); + } + // for Quarkus' RecordableConstructor @SuppressWarnings("unused") public OnAddFilter getOnAddFilter() { diff --git a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/RunTimeControllerConfiguration.java b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/RunTimeControllerConfiguration.java index d60d36a5d..479c727fb 100644 --- a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/RunTimeControllerConfiguration.java +++ b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/RunTimeControllerConfiguration.java @@ -2,9 +2,11 @@ import static io.quarkiverse.operatorsdk.runtime.Constants.QOSDK_USE_BUILDTIME_NAMESPACES; +import java.time.Duration; import java.util.List; import java.util.Optional; +import io.javaoperatorsdk.operator.api.reconciler.MaxReconciliationInterval; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @@ -35,8 +37,17 @@ public class RunTimeControllerConfiguration { /** * An optional list of comma-separated label selectors that Custom Resources must match to trigger the controller. - * See https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more details on selectors. + * See ... for more details on + * selectors. */ @ConfigItem public Optional selector; + + /** + * An optional {@link Duration} to specify the maximum time that is allowed to elapse before a reconciliation will happen + * regardless of the presence of events. See {@link MaxReconciliationInterval#interval()} for more details. + * Value is specified according to the rules defined at {@link Duration#parse(CharSequence)}. + */ + @ConfigItem + public Optional maxReconciliationInterval; } diff --git a/docs/modules/ROOT/pages/includes/quarkus-operator-sdk.adoc b/docs/modules/ROOT/pages/includes/quarkus-operator-sdk.adoc index bf4cdf33d..dbde59dee 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-operator-sdk.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-operator-sdk.adoc @@ -576,7 +576,7 @@ a| [[quarkus-operator-sdk_quarkus-operator-sdk-controllers-controllers-selector] [.description] -- -An optional list of comma-separated label selectors that Custom Resources must match to trigger the controller. See https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more details on selectors. +An optional list of comma-separated label selectors that Custom Resources must match to trigger the controller. See link:https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/[...] for more details on selectors. ifdef::add-copy-button-to-env-var[] Environment variable: env_var_with_copy_button:+++QUARKUS_OPERATOR_SDK_CONTROLLERS__CONTROLLERS__SELECTOR+++[] @@ -587,6 +587,24 @@ endif::add-copy-button-to-env-var[] --|string | + +a| [[quarkus-operator-sdk_quarkus-operator-sdk-controllers-controllers-max-reconciliation-interval]]`link:#quarkus-operator-sdk_quarkus-operator-sdk-controllers-controllers-max-reconciliation-interval[quarkus.operator-sdk.controllers."controllers".max-reconciliation-interval]` + + +[.description] +-- +An optional `Duration` to specify the maximum time that is allowed to elapse before a reconciliation will happen regardless of the presence of events. See `MaxReconciliationInterval++#++interval()` for more details. Value is specified according to the rules defined at `Duration++#++parse(CharSequence)`. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_OPERATOR_SDK_CONTROLLERS__CONTROLLERS__MAX_RECONCILIATION_INTERVAL+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_OPERATOR_SDK_CONTROLLERS__CONTROLLERS__MAX_RECONCILIATION_INTERVAL+++` +endif::add-copy-button-to-env-var[] +--|link:https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html[Duration] + link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]] +| + |=== ifndef::no-duration-note[] [NOTE] diff --git a/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/SecretReconciler.java b/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/SecretReconciler.java index 31ce4aabe..b3b1abd97 100644 --- a/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/SecretReconciler.java +++ b/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/SecretReconciler.java @@ -6,11 +6,13 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.MaxReconciliationInterval; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -@ControllerConfiguration +@ControllerConfiguration(name = SecretReconciler.NAME, maxReconciliationInterval = @MaxReconciliationInterval(interval = 2)) public class SecretReconciler implements Reconciler { + public static final String NAME = "secret"; @Override public UpdateControl reconcile(Secret secret, Context context) { diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties index 3dcf372fb..f0dd851e2 100644 --- a/integration-tests/src/main/resources/application.properties +++ b/integration-tests/src/main/resources/application.properties @@ -15,11 +15,14 @@ quarkus.operator-sdk.controllers.annotation.selector=environment=production,tier quarkus.operator-sdk.controllers.ApplicationScoped.namespaces=default quarkus.operator-sdk.controllers.variablens.namespaces=${VARIABLE_NS_ENV} quarkus.operator-sdk.controllers.name\ with\ space.namespaces=name-with-space +quarkus.operator-sdk.controllers.secret.max-reconciliation-interval=PT15M + quarkus.operator-sdk.concurrent-reconciliation-threads=10 quarkus.operator-sdk.termination-timeout-seconds=20 quarkus.operator-sdk.crd.validate=false quarkus.operator-sdk.crd.versions=v1beta1 quarkus.operator-sdk.activate-leader-election-for-profiles=prod,test,dev + ## activate to prevent the operator to start when debugging tests (note that some tests might fail because of this) quarkus.operator-sdk.start-operator=false quarkus.operator-sdk.enable-ssa=false \ No newline at end of file diff --git a/integration-tests/src/test/java/io/quarkiverse/operatorsdk/it/OperatorSDKResourceTest.java b/integration-tests/src/test/java/io/quarkiverse/operatorsdk/it/OperatorSDKResourceTest.java index eff48d651..0f25041b7 100644 --- a/integration-tests/src/test/java/io/quarkiverse/operatorsdk/it/OperatorSDKResourceTest.java +++ b/integration-tests/src/test/java/io/quarkiverse/operatorsdk/it/OperatorSDKResourceTest.java @@ -4,6 +4,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import java.time.Duration; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -98,14 +100,14 @@ void allControllersShouldHaveAssociatedConfiguration() { assertThat(names, arrayContainingInAnyOrder(ApplicationScopedReconciler.NAME, ConfiguredReconciler.NAME, TestReconciler.NAME, - ReconcilerUtils.getDefaultNameFor(SecretReconciler.class), ReconcilerUtils.getDefaultNameFor(GatewayReconciler.class), DependentDefiningReconciler.NAME, NamespaceFromEnvReconciler.NAME, EmptyReconciler.NAME, VariableNSReconciler.NAME, AnnotatedDependentReconciler.NAME, ReconcilerUtils.getDefaultNameFor(KeycloakController.class), NameWithSpaceReconciler.NAME, - CustomRateLimiterReconciler.NAME)); + CustomRateLimiterReconciler.NAME, + SecretReconciler.NAME)); } @Test @@ -286,4 +288,25 @@ void customRateLimiterConfiguredViaCustomAnnotationShouldWork() { "rateLimiter.value", equalTo(42), "itemStore.name", equalTo(NullItemStore.NAME)); } + + @Test + void shouldHaveDefaultMaxReconciliationInterval() { + given() + .when() + .get("/operator/" + EmptyReconciler.NAME + "/config") + .then() + .statusCode(200) + .body("maxReconciliationIntervalSeconds", equalTo(Long.valueOf(Duration.ofHours(10).getSeconds()).intValue())); + } + + @Test + void shouldUseMaxReconciliationIntervalFromPropertyIfProvided() { + given() + .when() + .get("/operator/" + SecretReconciler.NAME + "/config") + .then() + .statusCode(200) + .body("maxReconciliationIntervalSeconds", + equalTo(Long.valueOf(Duration.ofMinutes(15).getSeconds()).intValue())); + } }