diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index a637c72927..172f28e3f9 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -6,7 +6,7 @@ on: paths-ignore: - 'docs/**' - 'adr/**' - branches: [ main, next ] + branches: [ main, next, v5.3 ] push: paths-ignore: - 'docs/**' @@ -14,6 +14,7 @@ on: branches: - main - next + - v5.3 jobs: sample_operators_tests: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e889a0be13..a6bd5c2032 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -11,7 +11,7 @@ on: paths-ignore: - 'docs/**' - 'adr/**' - branches: [ main, v1, v2, v3, next ] + branches: [ main, v1, v2, v3, next, v5.3 ] workflow_dispatch: jobs: check_format_and_unit_tests: diff --git a/bootstrapper-maven-plugin/pom.xml b/bootstrapper-maven-plugin/pom.xml index b3271c1c4f..364495fca9 100644 --- a/bootstrapper-maven-plugin/pom.xml +++ b/bootstrapper-maven-plugin/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT bootstrapper diff --git a/caffeine-bounded-cache-support/pom.xml b/caffeine-bounded-cache-support/pom.xml index 8d3546a718..a050e0ff3c 100644 --- a/caffeine-bounded-cache-support/pom.xml +++ b/caffeine-bounded-cache-support/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT caffeine-bounded-cache-support diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index f1e174c856..93765e4a8a 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT micrometer-support diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index 2f3bef6fdf..1ffa1701f8 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk operator-framework-bom - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT pom Operator SDK - Bill of Materials Java SDK for implementing Kubernetes operators diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index bb1baad08b..361bb3ad53 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java index 7aa29c98ae..b56676ac45 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java @@ -258,7 +258,7 @@ public

RegisteredController

register( "Cannot register reconciler with name " + reconciler.getClass().getCanonicalName() + " reconciler named " - + ReconcilerUtils.getNameFor(reconciler) + + ReconcilerUtilsInternal.getNameFor(reconciler) + " because its configuration cannot be found.\n" + " Known reconcilers are: " + configurationService.getKnownReconcilerNames()); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternal.java similarity index 99% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternal.java index 354c2aa420..1523b792a5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternal.java @@ -34,7 +34,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @SuppressWarnings("rawtypes") -public class ReconcilerUtils { +public class ReconcilerUtilsInternal { private static final String FINALIZER_NAME_SUFFIX = "/finalizer"; protected static final String MISSING_GROUP_SUFFIX = ".javaoperatorsdk.io"; @@ -46,7 +46,7 @@ public class ReconcilerUtils { Pattern.compile(".*http(s?)://[^/]*/api(s?)/(\\S*).*"); // NOSONAR: input is controlled // prevent instantiation of util class - private ReconcilerUtils() {} + private ReconcilerUtilsInternal() {} public static boolean isFinalizerValid(String finalizer) { return HasMetadata.validateFinalizer(finalizer); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index b85ee03fcb..a1b37d6fe9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -22,7 +22,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; /** @@ -145,7 +145,7 @@ private String getReconcilersNameMessage() { } protected String keyFor(Reconciler reconciler) { - return ReconcilerUtils.getNameFor(reconciler); + return ReconcilerUtilsInternal.getNameFor(reconciler); } @SuppressWarnings("unused") diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java index 18ef8b598c..ca14043750 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java @@ -28,7 +28,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.Utils.Configurator; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; @@ -265,7 +265,7 @@ private

ResolvedControllerConfiguration

controllerCon io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration annotation) { final var resourceClass = getResourceClassResolver().getPrimaryResourceClass(reconcilerClass); - final var name = ReconcilerUtils.getNameFor(reconcilerClass); + final var name = ReconcilerUtilsInternal.getNameFor(reconcilerClass); final var generationAware = valueOrDefaultFromAnnotation( annotation, diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 6215c20179..6ed9b7ff64 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -28,8 +28,6 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.CustomResource; @@ -447,64 +445,6 @@ default Set> defaultNonSSAResource() { return defaultNonSSAResources(); } - /** - * If a javaoperatorsdk.io/previous annotation should be used so that the operator sdk can detect - * events from its own updates of dependent resources and then filter them. - * - *

Disable this if you want to react to your own dependent resource updates - * - * @return if special annotation should be used for dependent resource to filter events - * @since 4.5.0 - */ - default boolean previousAnnotationForDependentResourcesEventFiltering() { - return true; - } - - /** - * For dependent resources, the framework can add an annotation to filter out events resulting - * directly from the framework's operation. There are, however, some resources that do not follow - * the Kubernetes API conventions that changes in metadata should not increase the generation of - * the resource (as recorded in the {@code generation} field of the resource's {@code metadata}). - * For these resources, this convention is not respected and results in a new event for the - * framework to process. If that particular case is not handled correctly in the resource matcher, - * the framework will consider that the resource doesn't match the desired state and therefore - * triggers an update, which in turn, will re-add the annotation, thus starting the loop again, - * infinitely. - * - *

As a workaround, we automatically skip adding previous annotation for those well-known - * resources. Note that if you are sure that the matcher works for your use case, and it should in - * most instances, you can remove the resource type from the blocklist. - * - *

The consequence of adding a resource type to the set is that the framework will not use - * event filtering to prevent events, initiated by changes made by the framework itself as a - * result of its processing of dependent resources, to trigger the associated reconciler again. - * - *

Note that this method only takes effect if annotating dependent resources to prevent - * dependent resources events from triggering the associated reconciler again is activated as - * controlled by {@link #previousAnnotationForDependentResourcesEventFiltering()} - * - * @return a Set of resource classes where the previous version annotation won't be used. - */ - default Set> withPreviousAnnotationForDependentResourcesBlocklist() { - return Set.of(Deployment.class, StatefulSet.class); - } - - /** - * If the event logic should parse the resourceVersion to determine the ordering of dependent - * resource events. This is typically not needed. - * - *

Disabled by default as Kubernetes does not support, and discourages, this interpretation of - * resourceVersions. Enable only if your api server event processing seems to lag the operator - * logic, and you want to further minimize the amount of work done / updates issued by the - * operator. - * - * @return if resource version should be parsed (as integer) - * @since 4.5.0 - */ - default boolean parseResourceVersionsForEventFilteringAndCaching() { - return false; - } - /** * {@link io.javaoperatorsdk.operator.api.reconciler.UpdateControl} patch resource or status can * either use simple patches or SSA. Setting this to {@code true}, controllers will use SSA for diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index 3d29bb6589..cd9cdafb39 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -51,11 +51,8 @@ public class ConfigurationServiceOverrider { private Duration reconciliationTerminationTimeout; private Boolean ssaBasedCreateUpdateMatchForDependentResources; private Set> defaultNonSSAResource; - private Boolean previousAnnotationForDependentResources; - private Boolean parseResourceVersions; private Boolean useSSAToPatchPrimaryResource; private Boolean cloneSecondaryResourcesWhenGettingFromCache; - private Set> previousAnnotationUsageBlocklist; @SuppressWarnings("rawtypes") private DependentResourceFactory dependentResourceFactory; @@ -168,31 +165,6 @@ public ConfigurationServiceOverrider withDefaultNonSSAResource( return this; } - public ConfigurationServiceOverrider withPreviousAnnotationForDependentResources(boolean value) { - this.previousAnnotationForDependentResources = value; - return this; - } - - /** - * @param value true if internal algorithms can use metadata.resourceVersion as a numeric value. - * @return this - */ - public ConfigurationServiceOverrider withParseResourceVersions(boolean value) { - this.parseResourceVersions = value; - return this; - } - - /** - * @deprecated use withParseResourceVersions - * @param value true if internal algorithms can use metadata.resourceVersion as a numeric value. - * @return this - */ - @Deprecated(forRemoval = true) - public ConfigurationServiceOverrider wihtParseResourceVersions(boolean value) { - this.parseResourceVersions = value; - return this; - } - public ConfigurationServiceOverrider withUseSSAToPatchPrimaryResource(boolean value) { this.useSSAToPatchPrimaryResource = value; return this; @@ -204,12 +176,6 @@ public ConfigurationServiceOverrider withCloneSecondaryResourcesWhenGettingFromC return this; } - public ConfigurationServiceOverrider withPreviousAnnotationForDependentResourcesBlocklist( - Set> blocklist) { - this.previousAnnotationUsageBlocklist = blocklist; - return this; - } - public ConfigurationService build() { return new BaseConfigurationService(original.getVersion(), cloner, client) { @Override @@ -331,20 +297,6 @@ public Set> defaultNonSSAResources() { defaultNonSSAResource, ConfigurationService::defaultNonSSAResources); } - @Override - public boolean previousAnnotationForDependentResourcesEventFiltering() { - return overriddenValueOrDefault( - previousAnnotationForDependentResources, - ConfigurationService::previousAnnotationForDependentResourcesEventFiltering); - } - - @Override - public boolean parseResourceVersionsForEventFilteringAndCaching() { - return overriddenValueOrDefault( - parseResourceVersions, - ConfigurationService::parseResourceVersionsForEventFilteringAndCaching); - } - @Override public boolean useSSAToPatchPrimaryResource() { return overriddenValueOrDefault( @@ -357,14 +309,6 @@ public boolean cloneSecondaryResourcesWhenGettingFromCache() { cloneSecondaryResourcesWhenGettingFromCache, ConfigurationService::cloneSecondaryResourcesWhenGettingFromCache); } - - @Override - public Set> - withPreviousAnnotationForDependentResourcesBlocklist() { - return overriddenValueOrDefault( - previousAnnotationUsageBlocklist, - ConfigurationService::withPreviousAnnotationForDependentResourcesBlocklist); - } }; } } 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 a601092454..e0834621fc 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 @@ -20,7 +20,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec; import io.javaoperatorsdk.operator.api.reconciler.MaxReconciliationInterval; @@ -42,16 +42,18 @@ default String getName() { } default String getFinalizerName() { - return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); + return ReconcilerUtilsInternal.getDefaultFinalizerName(getResourceClass()); } static String ensureValidName(String name, String reconcilerClassName) { - return name != null ? name : ReconcilerUtils.getDefaultReconcilerName(reconcilerClassName); + return name != null + ? name + : ReconcilerUtilsInternal.getDefaultReconcilerName(reconcilerClassName); } static String ensureValidFinalizerName(String finalizer, String resourceTypeName) { if (finalizer != null && !finalizer.isBlank()) { - if (ReconcilerUtils.isFinalizerValid(finalizer)) { + if (ReconcilerUtilsInternal.isFinalizerValid(finalizer)) { return finalizer; } else { throw new IllegalArgumentException( @@ -61,7 +63,7 @@ static String ensureValidFinalizerName(String finalizer, String resourceTypeName + " for details"); } } else { - return ReconcilerUtils.getDefaultFinalizerName(resourceTypeName); + return ReconcilerUtilsInternal.getDefaultFinalizerName(resourceTypeName); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java index 9264db66bc..e6655641a2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java @@ -28,6 +28,7 @@ import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION; import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_LONG_VALUE_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_VALUE_SET; @@ -131,4 +132,11 @@ /** Kubernetes field selector for additional resource filtering */ Field[] fieldSelector() default {}; + + /** + * true if we can consider resource versions as integers, therefore it is valid to compare them + * + * @since 5.3.0 + */ + boolean comparableResourceVersions() default DEFAULT_COMPARABLE_RESOURCE_VERSION; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 24f78eb7be..f6caa4fe4d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -25,7 +25,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.informers.cache.ItemStore; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.reconciler.Constants; @@ -53,6 +53,7 @@ public class InformerConfiguration { private ItemStore itemStore; private Long informerListLimit; private FieldSelector fieldSelector; + private boolean comparableResourceVersions; protected InformerConfiguration( Class resourceClass, @@ -66,7 +67,8 @@ protected InformerConfiguration( GenericFilter genericFilter, ItemStore itemStore, Long informerListLimit, - FieldSelector fieldSelector) { + FieldSelector fieldSelector, + boolean comparableResourceVersions) { this(resourceClass); this.name = name; this.namespaces = namespaces; @@ -79,6 +81,7 @@ protected InformerConfiguration( this.itemStore = itemStore; this.informerListLimit = informerListLimit; this.fieldSelector = fieldSelector; + this.comparableResourceVersions = comparableResourceVersions; } private InformerConfiguration(Class resourceClass) { @@ -89,7 +92,7 @@ private InformerConfiguration(Class resourceClass) { // controller // where GenericKubernetesResource now does not apply ? GenericKubernetesResource.class.getSimpleName() - : ReconcilerUtils.getResourceTypeName(resourceClass); + : ReconcilerUtilsInternal.getResourceTypeName(resourceClass); } @SuppressWarnings({"rawtypes", "unchecked"}) @@ -113,7 +116,8 @@ public static InformerConfiguration.Builder builder( original.genericFilter, original.itemStore, original.informerListLimit, - original.fieldSelector) + original.fieldSelector, + original.comparableResourceVersions) .builder; } @@ -288,6 +292,10 @@ public FieldSelector getFieldSelector() { return fieldSelector; } + public boolean isComparableResourceVersions() { + return comparableResourceVersions; + } + @SuppressWarnings("UnusedReturnValue") public class Builder { @@ -359,6 +367,7 @@ public InformerConfiguration.Builder initFromAnnotation( Arrays.stream(informerConfig.fieldSelector()) .map(f -> new FieldSelector.Field(f.path(), f.value(), f.negated())) .toList())); + withComparableResourceVersions(informerConfig.comparableResourceVersions()); } return this; } @@ -459,5 +468,10 @@ public Builder withFieldSelector(FieldSelector fieldSelector) { InformerConfiguration.this.fieldSelector = fieldSelector; return this; } + + public Builder withComparableResourceVersions(boolean comparableResourceVersions) { + InformerConfiguration.this.comparableResourceVersions = comparableResourceVersions; + return this; + } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java index bca605a41c..69903e805f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java @@ -33,6 +33,7 @@ import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION; import static io.javaoperatorsdk.operator.api.reconciler.Constants.SAME_AS_CONTROLLER_NAMESPACES_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_ALL_NAMESPACE_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE_SET; @@ -96,18 +97,21 @@ class DefaultInformerEventSourceConfiguration private final GroupVersionKind groupVersionKind; private final InformerConfiguration informerConfig; private final KubernetesClient kubernetesClient; + private final boolean comparableResourceVersion; protected DefaultInformerEventSourceConfiguration( GroupVersionKind groupVersionKind, PrimaryToSecondaryMapper primaryToSecondaryMapper, SecondaryToPrimaryMapper secondaryToPrimaryMapper, InformerConfiguration informerConfig, - KubernetesClient kubernetesClient) { + KubernetesClient kubernetesClient, + boolean comparableResourceVersion) { this.informerConfig = Objects.requireNonNull(informerConfig); this.groupVersionKind = groupVersionKind; this.primaryToSecondaryMapper = primaryToSecondaryMapper; this.secondaryToPrimaryMapper = secondaryToPrimaryMapper; this.kubernetesClient = kubernetesClient; + this.comparableResourceVersion = comparableResourceVersion; } @Override @@ -135,6 +139,11 @@ public Optional getGroupVersionKind() { public Optional getKubernetesClient() { return Optional.ofNullable(kubernetesClient); } + + @Override + public boolean comparableResourceVersion() { + return this.comparableResourceVersion; + } } @SuppressWarnings({"unused", "UnusedReturnValue"}) @@ -148,6 +157,7 @@ class Builder { private PrimaryToSecondaryMapper primaryToSecondaryMapper; private SecondaryToPrimaryMapper secondaryToPrimaryMapper; private KubernetesClient kubernetesClient; + private boolean comparableResourceVersion = DEFAULT_COMPARABLE_RESOURCE_VERSION; private Builder(Class resourceClass, Class primaryResourceClass) { this(resourceClass, primaryResourceClass, null); @@ -285,6 +295,11 @@ public Builder withFieldSelector(FieldSelector fieldSelector) { return this; } + public Builder withComparableResourceVersion(boolean comparableResourceVersion) { + this.comparableResourceVersion = comparableResourceVersion; + return this; + } + public void updateFrom(InformerConfiguration informerConfig) { if (informerConfig != null) { final var informerConfigName = informerConfig.getName(); @@ -324,7 +339,10 @@ public InformerEventSourceConfiguration build() { HasMetadata.getKind(primaryResourceClass), false)), config.build(), - kubernetesClient); + kubernetesClient, + comparableResourceVersion); } } + + boolean comparableResourceVersion(); } 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 052b4d8c44..7330a407c1 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 @@ -41,6 +41,7 @@ public final class Constants { public static final String RESOURCE_GVK_KEY = "josdk.resource.gvk"; public static final String CONTROLLER_NAME = "controller.name"; public static final boolean DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES = true; + public static final boolean DEFAULT_COMPARABLE_RESOURCE_VERSION = true; private Constants() {} } 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 f3fade4659..f1aeadd52a 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 @@ -15,15 +15,19 @@ */ package io.javaoperatorsdk.operator.api.reconciler; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.function.Function; 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.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedWorkflowAndDependentResourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; @@ -41,6 +45,7 @@ public class DefaultContext

implements Context

{ defaultManagedDependentResourceContext; private final boolean primaryResourceDeleted; private final boolean primaryResourceFinalStateUnknown; + private final Map, Object> desiredStates = new ConcurrentHashMap<>(); public DefaultContext( RetryInfo retryInfo, @@ -157,4 +162,12 @@ public DefaultContext

setRetryInfo(RetryInfo retryInfo) { this.retryInfo = retryInfo; return this; } + + @SuppressWarnings("unchecked") + public R getOrComputeDesiredStateFor( + DependentResource dependentResource, Function desiredStateComputer) { + return (R) + desiredStates.computeIfAbsent( + dependentResource, ignored -> desiredStateComputer.apply(getPrimaryResource())); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java new file mode 100644 index 0000000000..8c240fabf7 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java @@ -0,0 +1,987 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.lang.reflect.InvocationTargetException; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; + +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; + +public class ReconcileUtils { + + private static final Logger log = LoggerFactory.getLogger(ReconcileUtils.class); + + public static final int DEFAULT_MAX_RETRY = 10; + + private ReconcileUtils() {} + + // todo move finalizers mtehods & deprecate + // todo test namespace handling + // todo compare resource version if multiple event sources provide the same resource + + /** + * Server-Side Apply the resource and filters out the resulting event. This is a convenience + * method that calls {@link #serverSideApply(Context, HasMetadata, boolean)} with filterEvent set + * to true. + * + * @param context of reconciler + * @param resource fresh resource for server side apply + * @return updated resource + * @param resource type + * @see #serverSideApply(Context, HasMetadata, boolean) + */ + public static R serverSideApply( + Context context, R resource) { + return serverSideApply(context, resource, true); + } + + /** + * Updates the resource and caches the response if needed, thus making sure that next + * reconciliation will contain to updated resource. Or more recent one if someone did an update + * after our update. + * + *

Optionally also can filter out the event, what is the result of this update. + * + *

You are free to control the optimistic locking by setting the resource version in resource + * metadata. In case of SSA we advise not to do updates with optimistic locking. + * + * @param context of reconciler + * @param resource fresh resource for server side apply + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R serverSideApply( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + context, + resource, + r -> + context + .getClient() + .resource(r) + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()), + filterEvent); + } + + /** + * Server-Side Apply the resource status subresource and filters out the resulting event. This is + * a convenience method that calls {@link #serverSideApplyStatus(Context, HasMetadata, boolean)} + * with filterEvent set to true. + * + * @param context of reconciler + * @param resource fresh resource for server side apply + * @return updated resource + * @param resource type + * @see #serverSideApplyStatus(Context, HasMetadata, boolean) + */ + public static R serverSideApplyStatus( + Context context, R resource) { + return serverSideApplyStatus(context, resource, true); + } + + /** + * Server-Side Apply the resource status subresource. Updates the resource status and caches the + * response if needed, ensuring the next reconciliation will contain the updated resource. + * + * @param context of reconciler + * @param resource fresh resource for server side apply + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R serverSideApplyStatus( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + context, + resource, + r -> + context + .getClient() + .resource(r) + .subresource("status") + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()), + filterEvent); + } + + /** + * Server-Side Apply the primary resource and filters out the resulting event. This is a + * convenience method that calls {@link #serverSideApplyPrimary(Context, HasMetadata, boolean)} + * with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource for server side apply + * @return updated resource + * @param

primary resource type + * @see #serverSideApplyPrimary(Context, HasMetadata, boolean) + */ + public static

P serverSideApplyPrimary(Context

context, P resource) { + return serverSideApplyPrimary(context, resource, true); + } + + /** + * Server-Side Apply the primary resource. Updates the primary resource and caches the response + * using the controller's event source, ensuring the next reconciliation will contain the updated + * resource. + * + * @param context of reconciler + * @param resource primary resource for server side apply + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param

primary resource type + */ + public static

P serverSideApplyPrimary( + Context

context, P resource, boolean filterEvent) { + return resourcePatch( + resource, + r -> + context + .getClient() + .resource(r) + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); + } + + /** + * Server-Side Apply the primary resource status subresource and filters out the resulting event. + * This is a convenience method that calls {@link #serverSideApplyPrimaryStatus(Context, + * HasMetadata, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource for server side apply + * @return updated resource + * @param

primary resource type + * @see #serverSideApplyPrimaryStatus(Context, HasMetadata, boolean) + */ + public static

P serverSideApplyPrimaryStatus( + Context

context, P resource) { + return serverSideApplyPrimaryStatus(context, resource, true); + } + + /** + * Server-Side Apply the primary resource status subresource. Updates the primary resource status + * and caches the response using the controller's event source. + * + * @param context of reconciler + * @param resource primary resource for server side apply + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param

primary resource type + */ + public static

P serverSideApplyPrimaryStatus( + Context

context, P resource, boolean filterEvent) { + return resourcePatch( + resource, + r -> + context + .getClient() + .resource(r) + .subresource("status") + .patch( + new PatchContext.Builder() + .withForce(true) + .withFieldManager(context.getControllerConfiguration().fieldManager()) + .withPatchType(PatchType.SERVER_SIDE_APPLY) + .build()), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); + } + + /** + * Updates the resource and filters out the resulting event. This is a convenience method that + * calls {@link #update(Context, HasMetadata, boolean)} with filterEvent set to true. Uses + * optimistic locking based on the resource version. + * + * @param context of reconciler + * @param resource resource to update + * @return updated resource + * @param resource type + * @see #update(Context, HasMetadata, boolean) + */ + public static R update( + Context context, R resource) { + return update(context, resource, true); + } + + /** + * Updates the resource with optimistic locking based on the resource version. Caches the response + * if needed, ensuring the next reconciliation will contain the updated resource. + * + * @param context of reconciler + * @param resource resource to update + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R update( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + context, resource, r -> context.getClient().resource(r).update(), filterEvent); + } + + /** + * Updates the resource status subresource and filters out the resulting event. This is a + * convenience method that calls {@link #updateStatus(Context, HasMetadata, boolean)} with + * filterEvent set to true. + * + * @param context of reconciler + * @param resource resource to update + * @return updated resource + * @param resource type + * @see #updateStatus(Context, HasMetadata, boolean) + */ + public static R updateStatus( + Context context, R resource) { + return updateStatus(context, resource, true); + } + + /** + * Updates the resource status subresource with optimistic locking. Caches the response if needed. + * + * @param context of reconciler + * @param resource resource to update + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R updateStatus( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + context, resource, r -> context.getClient().resource(r).updateStatus(), filterEvent); + } + + /** + * Updates the primary resource and filters out the resulting event. This is a convenience method + * that calls {@link #updatePrimary(Context, HasMetadata, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to update + * @return updated resource + * @param resource type + * @see #updatePrimary(Context, HasMetadata, boolean) + */ + public static R updatePrimary( + Context context, R resource) { + return updatePrimary(context, resource, true); + } + + /** + * Updates the primary resource with optimistic locking. Caches the response using the + * controller's event source. + * + * @param context of reconciler + * @param resource primary resource to update + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R updatePrimary( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).update(), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); + } + + /** + * Updates the primary resource status subresource and filters out the resulting event. This is a + * convenience method that calls {@link #updatePrimaryStatus(Context, HasMetadata, boolean)} with + * filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to update + * @return updated resource + * @param resource type + * @see #updatePrimaryStatus(Context, HasMetadata, boolean) + */ + public static R updatePrimaryStatus( + Context context, R resource) { + return updatePrimaryStatus(context, resource, true); + } + + /** + * Updates the primary resource status subresource with optimistic locking. Caches the response + * using the controller's event source. + * + * @param context of reconciler + * @param resource primary resource to update + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R updatePrimaryStatus( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).updateStatus(), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); + } + + /** + * Applies a JSON Patch to the resource and filters out the resulting event. This is a convenience + * method that calls {@link #jsonPatch(Context, HasMetadata, UnaryOperator, boolean)} with + * filterEvent set to true. + * + * @param context of reconciler + * @param resource resource to patch + * @param unaryOperator function to modify the resource + * @return updated resource + * @param resource type + * @see #jsonPatch(Context, HasMetadata, UnaryOperator, boolean) + */ + public static R jsonPatch( + Context context, R resource, UnaryOperator unaryOperator) { + return jsonPatch(context, resource, unaryOperator, true); + } + + /** + * Applies a JSON Patch to the resource. The unaryOperator function is used to modify the + * resource, and the differences are sent as a JSON Patch to the Kubernetes API server. + * + * @param context of reconciler + * @param resource resource to patch + * @param unaryOperator function to modify the resource + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R jsonPatch( + Context context, + R resource, + UnaryOperator unaryOperator, + boolean filterEvent) { + return resourcePatch( + context, resource, r -> context.getClient().resource(r).edit(unaryOperator), filterEvent); + } + + /** + * Applies a JSON Patch to the resource status subresource and filters out the resulting event. + * This is a convenience method that calls {@link #jsonPatchStatus(Context, HasMetadata, + * UnaryOperator, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource resource to patch + * @param unaryOperator function to modify the resource + * @return updated resource + * @param resource type + * @see #jsonPatchStatus(Context, HasMetadata, UnaryOperator, boolean) + */ + public static R jsonPatchStatus( + Context context, R resource, UnaryOperator unaryOperator) { + return jsonPatchStatus(context, resource, unaryOperator, true); + } + + /** + * Applies a JSON Patch to the resource status subresource. The unaryOperator function is used to + * modify the resource status, and the differences are sent as a JSON Patch. + * + * @param context of reconciler + * @param resource resource to patch + * @param unaryOperator function to modify the resource + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R jsonPatchStatus( + Context context, + R resource, + UnaryOperator unaryOperator, + boolean filterEvent) { + return resourcePatch( + context, + resource, + r -> context.getClient().resource(r).editStatus(unaryOperator), + filterEvent); + } + + /** + * Applies a JSON Patch to the primary resource and filters out the resulting event. This is a + * convenience method that calls {@link #jsonPatchPrimary(Context, HasMetadata, UnaryOperator, + * boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param unaryOperator function to modify the resource + * @return updated resource + * @param resource type + * @see #jsonPatchPrimary(Context, HasMetadata, UnaryOperator, boolean) + */ + public static R jsonPatchPrimary( + Context context, R resource, UnaryOperator unaryOperator) { + return jsonPatchPrimary(context, resource, unaryOperator, true); + } + + /** + * Applies a JSON Patch to the primary resource. Caches the response using the controller's event + * source. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param unaryOperator function to modify the resource + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R jsonPatchPrimary( + Context context, + R resource, + UnaryOperator unaryOperator, + boolean filterEvent) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).edit(unaryOperator), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); + } + + /** + * Applies a JSON Patch to the primary resource status subresource and filters out the resulting + * event. This is a convenience method that calls {@link #jsonPatchPrimaryStatus(Context, + * HasMetadata, UnaryOperator, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param unaryOperator function to modify the resource + * @return updated resource + * @param resource type + * @see #jsonPatchPrimaryStatus(Context, HasMetadata, UnaryOperator, boolean) + */ + public static R jsonPatchPrimaryStatus( + Context context, R resource, UnaryOperator unaryOperator) { + return jsonPatchPrimaryStatus(context, resource, unaryOperator, true); + } + + /** + * Applies a JSON Patch to the primary resource status subresource. Caches the response using the + * controller's event source. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param unaryOperator function to modify the resource + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R jsonPatchPrimaryStatus( + Context context, + R resource, + UnaryOperator unaryOperator, + boolean filterEvent) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).editStatus(unaryOperator), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); + } + + /** + * Applies a JSON Merge Patch to the resource and filters out the resulting event. This is a + * convenience method that calls {@link #jsonMergePatch(Context, HasMetadata, boolean)} with + * filterEvent set to true. JSON Merge Patch (RFC 7386) is a simpler patching strategy compared to + * JSON Patch. + * + * @param context of reconciler + * @param resource resource to patch + * @return updated resource + * @param resource type + * @see #jsonMergePatch(Context, HasMetadata, boolean) + */ + public static R jsonMergePatch( + Context context, R resource) { + return jsonMergePatch(context, resource, true); + } + + /** + * Applies a JSON Merge Patch to the resource. JSON Merge Patch (RFC 7386) is a simpler patching + * strategy that merges the provided resource with the existing resource on the server. + * + * @param context of reconciler + * @param resource resource to patch + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R jsonMergePatch( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + context, resource, r -> context.getClient().resource(r).patch(), filterEvent); + } + + /** + * Applies a JSON Merge Patch to the resource status subresource and filters out the resulting + * event. This is a convenience method that calls {@link #jsonMergePatchStatus(Context, + * HasMetadata, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource resource to patch + * @return updated resource + * @param resource type + * @see #jsonMergePatchStatus(Context, HasMetadata, boolean) + */ + public static R jsonMergePatchStatus( + Context context, R resource) { + return jsonMergePatchStatus(context, resource, true); + } + + /** + * Applies a JSON Merge Patch to the resource status subresource. Merges the provided resource + * status with the existing resource status on the server. + * + * @param context of reconciler + * @param resource resource to patch + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R jsonMergePatchStatus( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + context, resource, r -> context.getClient().resource(r).patchStatus(), filterEvent); + } + + /** + * Applies a JSON Merge Patch to the primary resource and filters out the resulting event. This is + * a convenience method that calls {@link #jsonMergePatchPrimary(Context, HasMetadata, boolean)} + * with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to patch + * @return updated resource + * @param resource type + * @see #jsonMergePatchPrimary(Context, HasMetadata, boolean) + */ + public static R jsonMergePatchPrimary( + Context context, R resource) { + return jsonMergePatchPrimary(context, resource, true); + } + + /** + * Applies a JSON Merge Patch to the primary resource. Caches the response using the controller's + * event source. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R jsonMergePatchPrimary( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).patch(), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); + } + + /** + * Applies a JSON Merge Patch to the primary resource status subresource and filters out the + * resulting event. This is a convenience method that calls {@link + * #jsonMergePatchPrimaryStatus(Context, HasMetadata, boolean)} with filterEvent set to true. + * + * @param context of reconciler + * @param resource primary resource to patch + * @return updated resource + * @param resource type + * @see #jsonMergePatchPrimaryStatus(Context, HasMetadata, boolean) + */ + public static R jsonMergePatchPrimaryStatus( + Context context, R resource) { + return jsonMergePatchPrimaryStatus(context, resource, true); + } + + /** + * Applies a JSON Merge Patch to the primary resource status subresource. Caches the response + * using the controller's event source. + * + * @param context of reconciler + * @param resource primary resource to patch + * @param filterEvent if true the event from this update will be filtered out so won't trigger the + * reconciliation + * @return updated resource + * @param resource type + */ + public static R jsonMergePatchPrimaryStatus( + Context context, R resource, boolean filterEvent) { + return resourcePatch( + resource, + r -> context.getClient().resource(r).patch(), + context.eventSourceRetriever().getControllerEventSource(), + filterEvent); + } + + /** + * Internal utility method to patch a resource and cache the result. Automatically discovers the + * event source for the resource type and delegates to {@link #resourcePatch(HasMetadata, + * UnaryOperator, ManagedInformerEventSource, boolean)}. + * + * @param context of reconciler + * @param resource resource to patch + * @param updateOperation operation to perform (update, patch, edit, etc.) + * @param filterEvent if true the event from this update will be filtered out + * @return updated resource + * @param resource type + * @throws IllegalStateException if no event source or multiple event sources are found + */ + public static R resourcePatch( + Context context, R resource, UnaryOperator updateOperation, boolean filterEvent) { + + var esList = context.eventSourceRetriever().getEventSourcesFor(resource.getClass()); + if (esList.isEmpty()) { + throw new IllegalStateException("No event source found for type: " + resource.getClass()); + } + if (esList.size() > 1) { + throw new IllegalStateException( + "Multiple event sources found for: " + + resource.getClass() + + " please provide the target event source"); + } + var es = esList.get(0); + if (es instanceof ManagedInformerEventSource mes) { + return resourcePatch(resource, updateOperation, mes, filterEvent); + } else { + throw new IllegalStateException( + "Target event source must be a subclass off " + + ManagedInformerEventSource.class.getName()); + } + } + + /** + * Internal utility method to patch a resource and cache the result using the specified event + * source. This method either filters out the resulting event or allows it to trigger + * reconciliation based on the filterEvent parameter. + * + * @param resource resource to patch + * @param updateOperation operation to perform (update, patch, edit, etc.) + * @param ies the managed informer event source to use for caching + * @param filterEvent if true the event from this update will be filtered out + * @return updated resource + * @param resource type + */ + @SuppressWarnings("unchecked") + public static R resourcePatch( + R resource, + UnaryOperator updateOperation, + ManagedInformerEventSource ies, + boolean filterEvent) { + return filterEvent + ? (R) ies.eventFilteringUpdateAndCacheResource(resource, updateOperation) + : (R) ies.updateAndCacheResource(resource, updateOperation); + } + + /** + * Adds the default finalizer (from controller configuration) to the primary resource. This is a + * convenience method that calls {@link #addFinalizer(Context, String)} with the configured + * finalizer name. + * + * @param context of reconciler + * @return updated resource from the server response + * @param

primary resource type + * @see #addFinalizer(Context, String) + */ + public static

P addFinalizer(Context

context) { + return addFinalizer(context, context.getControllerConfiguration().getFinalizerName()); + } + + /** + * Adds finalizer to the resource using JSON Patch. Retries conflicts and unprocessable content + * (HTTP 422), see {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, + * HasMetadata, UnaryOperator, Predicate)} for details on retry. It does not try to add finalizer + * if there is already a finalizer or resource is marked for deletion. + * + * @return updated resource from the server response + */ + public static

P addFinalizer(Context

context, String finalizerName) { + var resource = context.getPrimaryResource(); + if (resource.isMarkedForDeletion() || resource.hasFinalizer(finalizerName)) { + return resource; + } + return conflictRetryingPatch( + context, + r -> { + r.addFinalizer(finalizerName); + return r; + }, + r -> !r.hasFinalizer(finalizerName)); + } + + /** + * Removes the default finalizer (from controller configuration) from the primary resource. This + * is a convenience method that calls {@link #removeFinalizer(Context, String)} with the + * configured finalizer name. + * + * @param context of reconciler + * @return updated resource from the server response + * @param

primary resource type + * @see #removeFinalizer(Context, String) + */ + public static

P removeFinalizer(Context

context) { + return removeFinalizer(context, context.getControllerConfiguration().getFinalizerName()); + } + + /** + * Removes the target finalizer from target resource. Uses JSON Patch and handles retries, see + * {@link PrimaryUpdateAndCacheUtils#conflictRetryingPatch(KubernetesClient, HasMetadata, + * UnaryOperator, Predicate)} for details. It does not try to remove finalizer if finalizer is not + * present on the resource. + * + * @return updated resource from the server response + */ + public static

P removeFinalizer( + Context

context, String finalizerName) { + var resource = context.getPrimaryResource(); + if (!resource.hasFinalizer(finalizerName)) { + return resource; + } + return conflictRetryingPatch( + context, + r -> { + r.removeFinalizer(finalizerName); + return r; + }, + r -> r.hasFinalizer(finalizerName)); + } + + /** + * Patches the resource using JSON Patch. In case the server responds with conflict (HTTP 409) or + * unprocessable content (HTTP 422) it retries the operation up to the maximum number defined in + * {@link PrimaryUpdateAndCacheUtils#DEFAULT_MAX_RETRY}. + * + * @param context reconiciliation context + * @param resourceChangesOperator changes to be done on the resource before update + * @param preCondition condition to check if the patch operation still needs to be performed or + * not. + * @return updated resource from the server or unchanged if the precondition does not hold. + * @param

resource type + */ + @SuppressWarnings("unchecked") + private static

P conflictRetryingPatch( + Context

context, UnaryOperator

resourceChangesOperator, Predicate

preCondition) { + var resource = context.getPrimaryResource(); + var client = context.getClient(); + if (log.isDebugEnabled()) { + log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource)); + } + int retryIndex = 0; + while (true) { + try { + if (!preCondition.test(resource)) { + return resource; + } + return jsonPatchPrimary(context, resource, resourceChangesOperator, false); + } catch (KubernetesClientException e) { + log.trace("Exception during patch for resource: {}", resource); + retryIndex++; + // only retry on conflict (409) and unprocessable content (422) which + // can happen if JSON Patch is not a valid request since there was + // a concurrent request which already removed another finalizer: + // List element removal from a list is by index in JSON Patch + // so if addressing a second finalizer but first is meanwhile removed + // it is a wrong request. + if (e.getCode() != 409 && e.getCode() != 422) { + throw e; + } + if (retryIndex >= DEFAULT_MAX_RETRY) { + throw new OperatorException( + "Exceeded maximum (" + + DEFAULT_MAX_RETRY + + ") retry attempts to patch resource: " + + ResourceID.fromResource(resource)); + } + log.debug( + "Retrying patch for resource name: {}, namespace: {}; HTTP code: {}", + resource.getMetadata().getName(), + resource.getMetadata().getNamespace(), + e.getCode()); + var operation = client.resources(resource.getClass()); + if (resource.getMetadata().getNamespace() != null) { + resource = + (P) + operation + .inNamespace(resource.getMetadata().getNamespace()) + .withName(resource.getMetadata().getName()) + .get(); + } else { + resource = (P) operation.withName(resource.getMetadata().getName()).get(); + } + } + } + } + + /** + * Adds the default finalizer (from controller configuration) to the primary resource using + * Server-Side Apply. This is a convenience method that calls {@link #addFinalizerWithSSA(Context, + * String)} with the configured finalizer name. + * + * @param context of reconciler + * @return the patched resource from the server response + * @param

primary resource type + * @see #addFinalizerWithSSA(Context, String) + */ + public static

P addFinalizerWithSSA(Context

context) { + return addFinalizerWithSSA(context, context.getControllerConfiguration().getFinalizerName()); + } + + /** + * Adds finalizer using Server-Side Apply. In the background this method creates a fresh copy of + * the target resource, setting only name, namespace and finalizer. Does not use optimistic + * locking for the patch. + * + * @param context of reconciler + * @param finalizerName name of the finalizer to add + * @return the patched resource from the server response + * @param

primary resource type + */ + public static

P addFinalizerWithSSA( + Context

context, String finalizerName) { + var originalResource = context.getPrimaryResource(); + if (log.isDebugEnabled()) { + log.debug( + "Adding finalizer (using SSA) for resource: {} version: {}", + getUID(originalResource), + getVersion(originalResource)); + } + try { + P resource = (P) originalResource.getClass().getConstructor().newInstance(); + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName(originalResource.getMetadata().getName()); + objectMeta.setNamespace(originalResource.getMetadata().getNamespace()); + resource.setMetadata(objectMeta); + resource.addFinalizer(finalizerName); + + return serverSideApplyPrimary(context, resource, false); + } catch (InstantiationException + | IllegalAccessException + | InvocationTargetException + | NoSuchMethodException e) { + throw new RuntimeException( + "Issue with creating custom resource instance with reflection." + + " Custom Resources must provide a no-arg constructor. Class: " + + originalResource.getClass().getName(), + e); + } + } + + /** + * Compares resource versions of two resources. This is a convenience method that extracts the + * resource versions from the metadata and delegates to {@link #compareResourceVersions(String, + * String)}. + * + * @param h1 first resource + * @param h2 second resource + * @return negative if h1 is older, zero if equal, positive if h1 is newer + * @throws NonComparableResourceVersionException if either resource version is invalid + */ + public static int compareResourceVersions(HasMetadata h1, HasMetadata h2) { + return compareResourceVersions( + h1.getMetadata().getResourceVersion(), h2.getMetadata().getResourceVersion()); + } + + /** + * Compares two Kubernetes resource versions numerically. Kubernetes resource versions are + * expected to be numeric strings that increase monotonically. This method assumes both versions + * are valid numeric strings without leading zeros. + * + * @param v1 first resource version + * @param v2 second resource version + * @return negative if v1 is older, zero if equal, positive if v1 is newer + * @throws NonComparableResourceVersionException if either resource version is empty, has leading + * zeros, or contains non-numeric characters + */ + public static int compareResourceVersions(String v1, String v2) { + int v1Length = validateResourceVersion(v1); + int v2Length = validateResourceVersion(v2); + int comparison = v1Length - v2Length; + if (comparison != 0) { + return comparison; + } + for (int i = 0; i < v2Length; i++) { + int comp = v1.charAt(i) - v2.charAt(i); + if (comp != 0) { + return comp; + } + } + return 0; + } + + private static int validateResourceVersion(String v1) { + int v1Length = v1.length(); + if (v1Length == 0) { + throw new NonComparableResourceVersionException("Resource version is empty"); + } + for (int i = 0; i < v1Length; i++) { + char char1 = v1.charAt(i); + if (char1 == '0') { + if (i == 0) { + throw new NonComparableResourceVersionException( + "Resource version cannot begin with 0: " + v1); + } + } else if (char1 < '0' || char1 > '9') { + throw new NonComparableResourceVersionException( + "Non numeric characters in resource version: " + v1); + } + } + return v1Length; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java new file mode 100644 index 0000000000..3e1a4f9b14 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcilerUtils.java @@ -0,0 +1,15 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java index a7c5ce9e2d..8dc62b4ca7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java @@ -23,6 +23,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @@ -85,7 +86,7 @@ protected ReconcileResult reconcile(P primary, R actualResource, Context

c if (creatable() || updatable()) { if (actualResource == null) { if (creatable) { - var desired = desired(primary, context); + var desired = getOrComputeDesired(context); throwIfNull(desired, primary, "Desired"); logForOperation("Creating", primary, desired); var createdResource = handleCreate(desired, primary, context); @@ -95,7 +96,8 @@ protected ReconcileResult reconcile(P primary, R actualResource, Context

c if (updatable()) { final Matcher.Result match = match(actualResource, primary, context); if (!match.matched()) { - final var desired = match.computedDesired().orElseGet(() -> desired(primary, context)); + final var desired = + match.computedDesired().orElseGet(() -> getOrComputeDesired(context)); throwIfNull(desired, primary, "Desired"); logForOperation("Updating", primary, desired); var updatedResource = handleUpdate(actualResource, desired, primary, context); @@ -127,7 +129,6 @@ protected ReconcileResult reconcile(P primary, R actualResource, Context

c @Override public Optional getSecondaryResource(P primary, Context

context) { - var secondaryResources = context.getSecondaryResources(resourceType()); if (secondaryResources.isEmpty()) { return Optional.empty(); @@ -212,6 +213,27 @@ protected R desired(P primary, Context

context) { + " updated"); } + /** + * Retrieves the desired state from the {@link Context} if it has already been computed or calls + * {@link #desired(HasMetadata, Context)} and stores its result in the context for further use. + * This ensures that {@code desired} is only called once per reconciliation to avoid unneeded + * processing and supports scenarios where idempotent computation of the desired state is not + * feasible. + * + *

Note that this method should normally only be called by the SDK itself and exclusively (i.e. + * {@link #desired(HasMetadata, Context)} should not be called directly by the SDK) whenever the + * desired state is needed to ensure it is properly cached for the current reconciliation. + * + * @param context the {@link Context} in scope for the current reconciliation + * @return the desired state associated with this dependent resource based on the currently + * in-scope primary resource as found in the context + */ + protected R getOrComputeDesired(Context

context) { + assert context instanceof DefaultContext

; + DefaultContext

defaultContext = (DefaultContext

) context; + return defaultContext.getOrComputeDesiredStateFor(this, p -> desired(p, defaultContext)); + } + public void delete(P primary, Context

context) { dependentResourceReconciler.delete(primary, context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java index e601e937cf..7b83a377c1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java @@ -105,7 +105,7 @@ protected void handleExplicitStateCreation(P primary, R created, Context

cont @Override public Matcher.Result match(R resource, P primary, Context

context) { - var desired = desired(primary, context); + var desired = getOrComputeDesired(context); return Matcher.Result.computed(resource.equals(desired), desired); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java index 5b3617c26c..23135f81b1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java @@ -27,7 +27,6 @@ import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; -import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; class BulkDependentResourceReconciler implements DependentResourceReconciler { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index 0ba48797af..5562c883e2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -138,7 +138,7 @@ public static Matcher.Result m Context

context, boolean labelsAndAnnotationsEquality, String... ignorePaths) { - final var desired = dependentResource.desired(primary, context); + final var desired = dependentResource.getOrComputeDesired(context); return match(desired, actualResource, labelsAndAnnotationsEquality, context, ignorePaths); } @@ -150,7 +150,7 @@ public static Matcher.Result m boolean specEquality, boolean labelsAndAnnotationsEquality, String... ignorePaths) { - final var desired = dependentResource.desired(primary, context); + final var desired = dependentResource.getOrComputeDesired(context); return match( desired, actualResource, labelsAndAnnotationsEquality, specEquality, context, ignorePaths); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 05cddcade1..b9ea27b190 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -55,7 +55,6 @@ public abstract class KubernetesDependentResource kubernetesDependentResourceConfig; private volatile Boolean useSSA; - private volatile Boolean usePreviousAnnotationForEventFiltering; public KubernetesDependentResource() {} @@ -72,6 +71,25 @@ public void configureWith(KubernetesDependentResourceConfig config) { this.kubernetesDependentResourceConfig = config; } + @Override + protected R handleCreate(R desired, P primary, Context

context) { + return eventSource() + .orElseThrow() + .eventFilteringUpdateAndCacheResource( + desired, + toCreate -> KubernetesDependentResource.super.handleCreate(toCreate, primary, context)); + } + + @Override + protected R handleUpdate(R actual, R desired, P primary, Context

context) { + return eventSource() + .orElseThrow() + .eventFilteringUpdateAndCacheResource( + desired, + toUpdate -> + KubernetesDependentResource.super.handleUpdate(actual, toUpdate, primary, context)); + } + @SuppressWarnings("unused") public R create(R desired, P primary, Context

context) { if (useSSA(context)) { @@ -123,7 +141,7 @@ public R update(R actual, R desired, P primary, Context

context) { @Override public Result match(R actualResource, P primary, Context

context) { - final var desired = desired(primary, context); + final var desired = getOrComputeDesired(context); return match(actualResource, desired, primary, context); } @@ -158,14 +176,6 @@ protected void addMetadata( } else { annotations.remove(InformerEventSource.PREVIOUS_ANNOTATION_KEY); } - } else if (usePreviousAnnotation(context)) { // set a new one - eventSource() - .orElseThrow() - .addPreviousAnnotation( - Optional.ofNullable(actualResource) - .map(r -> r.getMetadata().getResourceVersion()) - .orElse(null), - target); } addReferenceHandlingMetadata(target, primary); } @@ -181,22 +191,6 @@ protected boolean useSSA(Context

context) { return useSSA; } - private boolean usePreviousAnnotation(Context

context) { - if (usePreviousAnnotationForEventFiltering == null) { - usePreviousAnnotationForEventFiltering = - context - .getControllerConfiguration() - .getConfigurationService() - .previousAnnotationForDependentResourcesEventFiltering() - && !context - .getControllerConfiguration() - .getConfigurationService() - .withPreviousAnnotationForDependentResourcesBlocklist() - .contains(this.resourceType()); - } - return usePreviousAnnotationForEventFiltering; - } - @Override protected void handleDelete(P primary, R secondary, Context

context) { if (secondary != null) { @@ -301,7 +295,7 @@ protected Optional selectTargetSecondaryResource( * @return id of the target managed resource */ protected ResourceID targetSecondaryResourceID(P primary, Context

context) { - return ResourceID.fromResource(desired(primary, context)); + return ResourceID.fromResource(getOrComputeDesired(context)); } protected boolean addOwnerReference() { @@ -309,8 +303,8 @@ protected boolean addOwnerReference() { } @Override - protected R desired(P primary, Context

context) { - return super.desired(primary, context); + protected R getOrComputeDesired(Context

context) { + return super.getOrComputeDesired(context); } @Override 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 8a1ddb4c5a..a9e542ee8c 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 @@ -15,9 +15,7 @@ */ package io.javaoperatorsdk.operator.processing.event; -import java.lang.reflect.InvocationTargetException; import java.net.HttpURLConnection; -import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,20 +24,18 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.Namespaced; -import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import io.fabric8.kubernetes.client.dsl.base.PatchContext; -import io.fabric8.kubernetes.client.dsl.base.PatchType; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.BaseControl; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.Controller; @@ -49,8 +45,6 @@ /** Handles calls and results of a Reconciler and finalizer related logic */ class ReconciliationDispatcher

{ - public static final int MAX_UPDATE_RETRY = 10; - private static final Logger log = LoggerFactory.getLogger(ReconciliationDispatcher.class); private final Controller

controller; @@ -119,7 +113,7 @@ && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) { // checking the cleaner for all-event-mode if (!triggerOnAllEvent() && markedForDeletion) { - return handleCleanup(resourceForExecution, originalResource, context, executionScope); + return handleCleanup(resourceForExecution, context, executionScope); } else { return handleReconcile(executionScope, resourceForExecution, originalResource, context); } @@ -148,9 +142,9 @@ private PostExecutionControl

handleReconcile( */ P updatedResource; if (useSSA) { - updatedResource = addFinalizerWithSSA(originalResource); + updatedResource = ReconcileUtils.addFinalizerWithSSA(context); } else { - updatedResource = updateCustomResourceWithFinalizer(resourceForExecution, originalResource); + updatedResource = ReconcileUtils.addFinalizer(context); } return PostExecutionControl.onlyFinalizerAdded(updatedResource); } else { @@ -194,7 +188,7 @@ private PostExecutionControl

reconcileExecution( } if (updateControl.isPatchResource()) { - updatedCustomResource = patchResource(toUpdate, originalResource); + updatedCustomResource = patchResource(context, toUpdate, originalResource); if (!useSSA) { toUpdate .getMetadata() @@ -203,7 +197,7 @@ private PostExecutionControl

reconcileExecution( } if (updateControl.isPatchStatus()) { - customResourceFacade.patchStatus(toUpdate, originalResource); + customResourceFacade.patchStatus(context, toUpdate, originalResource); } return createPostExecutionControl(updatedCustomResource, updateControl, executionScope); } @@ -241,7 +235,7 @@ public boolean isLastAttempt() { try { updatedResource = customResourceFacade.patchStatus( - errorStatusUpdateControl.getResource().orElseThrow(), originalResource); + context, errorStatusUpdateControl.getResource().orElseThrow(), originalResource); } catch (Exception ex) { int code = ex instanceof KubernetesClientException kcex ? kcex.getCode() : -1; Level exceptionLevel = Level.ERROR; @@ -317,10 +311,7 @@ private void updatePostExecutionControlWithReschedule( } private PostExecutionControl

handleCleanup( - P resourceForExecution, - P originalResource, - Context

context, - ExecutionScope

executionScope) { + P resourceForExecution, Context

context, ExecutionScope

executionScope) { if (log.isDebugEnabled()) { log.debug( "Executing delete for resource: {} with version: {}", @@ -334,24 +325,7 @@ private PostExecutionControl

handleCleanup( // cleanup is finished, nothing left to be done final var finalizerName = configuration().getFinalizerName(); if (deleteControl.isRemoveFinalizer() && resourceForExecution.hasFinalizer(finalizerName)) { - P customResource = - conflictRetryingPatch( - resourceForExecution, - originalResource, - r -> { - // the operator might not be allowed to retrieve the resource on a retry, e.g. - // when its - // permissions are removed by deleting the namespace concurrently - if (r == null) { - log.warn( - "Could not remove finalizer on null resource: {} with version: {}", - getUID(resourceForExecution), - getVersion(resourceForExecution)); - return false; - } - return r.removeFinalizer(finalizerName); - }, - true); + P customResource = ReconcileUtils.removeFinalizer(context); return PostExecutionControl.customResourceFinalizerRemoved(customResource); } } @@ -367,45 +341,7 @@ private PostExecutionControl

handleCleanup( return postExecutionControl; } - @SuppressWarnings("unchecked") - private P addFinalizerWithSSA(P originalResource) { - log.debug( - "Adding finalizer (using SSA) for resource: {} version: {}", - getUID(originalResource), - getVersion(originalResource)); - try { - P resource = (P) originalResource.getClass().getConstructor().newInstance(); - ObjectMeta objectMeta = new ObjectMeta(); - objectMeta.setName(originalResource.getMetadata().getName()); - objectMeta.setNamespace(originalResource.getMetadata().getNamespace()); - resource.setMetadata(objectMeta); - resource.addFinalizer(configuration().getFinalizerName()); - return customResourceFacade.patchResourceWithSSA(resource); - } catch (InstantiationException - | IllegalAccessException - | InvocationTargetException - | NoSuchMethodException e) { - throw new RuntimeException( - "Issue with creating custom resource instance with reflection." - + " Custom Resources must provide a no-arg constructor. Class: " - + originalResource.getClass().getName(), - e); - } - } - - private P updateCustomResourceWithFinalizer(P resourceForExecution, P originalResource) { - log.debug( - "Adding finalizer for resource: {} version: {}", - getUID(originalResource), - getVersion(originalResource)); - return conflictRetryingPatch( - resourceForExecution, - originalResource, - r -> r.addFinalizer(configuration().getFinalizerName()), - false); - } - - private P patchResource(P resource, P originalResource) { + private P patchResource(Context

context, P resource, P originalResource) { log.debug( "Updating resource: {} with version: {}; SSA: {}", getUID(resource), @@ -418,64 +354,13 @@ private P patchResource(P resource, P originalResource) { // addFinalizer already prevents adding an already present finalizer so no need to check resource.addFinalizer(finalizerName); } - return customResourceFacade.patchResource(resource, originalResource); + return customResourceFacade.patchResource(context, resource, originalResource); } ControllerConfiguration

configuration() { return controller.getConfiguration(); } - public P conflictRetryingPatch( - P resource, - P originalResource, - Function modificationFunction, - boolean forceNotUseSSA) { - if (log.isDebugEnabled()) { - log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource)); - } - int retryIndex = 0; - while (true) { - try { - var modified = modificationFunction.apply(resource); - if (Boolean.FALSE.equals(modified)) { - return resource; - } - if (forceNotUseSSA) { - return customResourceFacade.patchResourceWithoutSSA(resource, originalResource); - } else { - return customResourceFacade.patchResource(resource, originalResource); - } - } catch (KubernetesClientException e) { - log.trace("Exception during patch for resource: {}", resource); - retryIndex++; - // only retry on conflict (409) and unprocessable content (422) which - // can happen if JSON Patch is not a valid request since there was - // a concurrent request which already removed another finalizer: - // List element removal from a list is by index in JSON Patch - // so if addressing a second finalizer but first is meanwhile removed - // it is a wrong request. - if (e.getCode() != 409 && e.getCode() != 422) { - throw e; - } - if (retryIndex >= MAX_UPDATE_RETRY) { - throw new OperatorException( - "Exceeded maximum (" - + MAX_UPDATE_RETRY - + ") retry attempts to patch resource: " - + ResourceID.fromResource(resource)); - } - log.debug( - "Retrying patch for resource name: {}, namespace: {}; HTTP code: {}", - resource.getMetadata().getName(), - resource.getMetadata().getNamespace(), - e.getCode()); - resource = - customResourceFacade.getResource( - resource.getMetadata().getNamespace(), resource.getMetadata().getName()); - } - } - } - private void validateExecutionScope(ExecutionScope

executionScope) { if (!triggerOnAllEvent() && (executionScope.isDeleteEvent() || executionScope.isDeleteFinalStateUnknown())) { @@ -490,7 +375,6 @@ static class CustomResourceFacade { private final MixedOperation, Resource> resourceOperation; private final boolean useSSA; - private final String fieldManager; private final Cloner cloner; public CustomResourceFacade( @@ -499,7 +383,6 @@ public CustomResourceFacade( Cloner cloner) { this.resourceOperation = resourceOperation; this.useSSA = configuration.getConfigurationService().useSSAToPatchPrimaryResource(); - this.fieldManager = configuration.fieldManager(); this.cloner = cloner; } @@ -515,7 +398,7 @@ public R patchResourceWithoutSSA(R resource, R originalResource) { return resource(originalResource).edit(r -> resource); } - public R patchResource(R resource, R originalResource) { + public R patchResource(Context context, R resource, R originalResource) { if (log.isDebugEnabled()) { log.debug( "Trying to replace resource {}, version: {}", @@ -523,35 +406,28 @@ public R patchResource(R resource, R originalResource) { resource.getMetadata().getResourceVersion()); } if (useSSA) { - return patchResourceWithSSA(resource); + return ReconcileUtils.serverSideApplyPrimary(context, resource); } else { - return resource(originalResource).edit(r -> resource); + return ReconcileUtils.jsonPatchPrimary(context, originalResource, r -> resource); } } - public R patchStatus(R resource, R originalResource) { + public R patchStatus(Context context, R resource, R originalResource) { log.trace("Patching status for resource: {} with ssa: {}", resource, useSSA); if (useSSA) { var managedFields = resource.getMetadata().getManagedFields(); try { resource.getMetadata().setManagedFields(null); - var res = resource(resource); - return res.subresource("status") - .patch( - new PatchContext.Builder() - .withFieldManager(fieldManager) - .withForce(true) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build()); + return ReconcileUtils.serverSideApplyPrimaryStatus(context, resource); } finally { resource.getMetadata().setManagedFields(managedFields); } } else { - return editStatus(resource, originalResource); + return editStatus(context, resource, originalResource); } } - private R editStatus(R resource, R originalResource) { + private R editStatus(Context context, R resource, R originalResource) { String resourceVersion = resource.getMetadata().getResourceVersion(); // the cached resource should not be changed in any circumstances // that can lead to all kinds of race conditions. @@ -559,10 +435,11 @@ private R editStatus(R resource, R originalResource) { try { clonedOriginal.getMetadata().setResourceVersion(null); resource.getMetadata().setResourceVersion(null); - var res = resource(clonedOriginal); - return res.editStatus( + return ReconcileUtils.jsonPatchPrimaryStatus( + context, + clonedOriginal, r -> { - ReconcilerUtils.setStatus(r, ReconcilerUtils.getStatus(resource)); + ReconcilerUtilsInternal.setStatus(r, ReconcilerUtilsInternal.getStatus(resource)); return r; }); } finally { @@ -572,16 +449,6 @@ private R editStatus(R resource, R originalResource) { } } - public R patchResourceWithSSA(R resource) { - return resource(resource) - .patch( - new PatchContext.Builder() - .withFieldManager(fieldManager) - .withForce(true) - .withPatchType(PatchType.SERVER_SIDE_APPLY) - .build()); - } - private Resource resource(R resource) { return resource instanceof Namespaced ? resourceOperation.inNamespace(resource.getMetadata().getNamespace()).resource(resource) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java new file mode 100644 index 0000000000..3e1a4f9b14 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java @@ -0,0 +1,15 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java index b7a6406e20..3c232ceb07 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java @@ -32,7 +32,7 @@ import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; -import static io.javaoperatorsdk.operator.ReconcilerUtils.handleKubernetesClientException; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.handleKubernetesClientException; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; import static io.javaoperatorsdk.operator.processing.event.source.controller.InternalEventFilters.*; @@ -47,7 +47,11 @@ public class ControllerEventSource @SuppressWarnings({"unchecked", "rawtypes"}) public ControllerEventSource(Controller controller) { - super(NAME, controller.getCRClient(), controller.getConfiguration(), false); + super( + NAME, + controller.getCRClient(), + controller.getConfiguration(), + controller.getConfiguration().getInformerConfig().isComparableResourceVersions()); this.controller = controller; final var config = controller.getConfiguration(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index ec11db25f4..8f14f4961e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -17,7 +17,6 @@ import java.util.Optional; import java.util.Set; -import java.util.UUID; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -28,43 +27,19 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION; + /** * Wraps informer(s) so they are connected to the eventing system of the framework. Note that since * this is built on top of Fabric8 client Informers, it also supports caching resources using - * caching from informer caches as well as additional caches described below. - * - *

InformerEventSource also supports two features to better handle events and caching of - * resources on top of Informers from the Fabric8 Kubernetes client. These two features are related - * to each other as follows: - * - *

    - *
  1. Ensuring the cache contains the fresh resource after an update. This is important for - * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} and mainly - * for {@link - * io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource} so - * that {@link - * io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource#getSecondaryResource(HasMetadata, - * Context)} always returns the latest version of the resource after a reconciliation. To - * achieve this {@link #handleRecentResourceUpdate(ResourceID, HasMetadata, HasMetadata)} and - * {@link #handleRecentResourceCreate(ResourceID, HasMetadata)} need to be called explicitly - * after a resource is created or updated using the kubernetes client. These calls are done - * automatically by the KubernetesDependentResource implementation. In the background this - * will store the new resource in a temporary cache {@link TemporaryResourceCache} which does - * additional checks. After a new event is received the cached object is removed from this - * cache, since it is then usually already in the informer cache. - *
  2. Avoiding unneeded reconciliations after resources are created or updated. This filters out - * events that are the results of updates and creates made by the controller itself because we - * typically don't want the associated informer to trigger an event causing a useless - * reconciliation (as the change originates from the reconciler itself). For the details see - * {@link #canSkipEvent(HasMetadata, HasMetadata, ResourceID)} and related usage. - *
+ * caching from informer caches as well as filtering events which are result of the controller's + * update. * * @param resource type being watched * @param

type of the associated primary resource @@ -78,28 +53,24 @@ public class InformerEventSource // we need direct control for the indexer to propagate the just update resource also to the index private final PrimaryToSecondaryIndex primaryToSecondaryIndex; private final PrimaryToSecondaryMapper

primaryToSecondaryMapper; - private final String id = UUID.randomUUID().toString(); public InformerEventSource( InformerEventSourceConfiguration configuration, EventSourceContext

context) { this( configuration, configuration.getKubernetesClient().orElse(context.getClient()), - context - .getControllerConfiguration() - .getConfigurationService() - .parseResourceVersionsForEventFilteringAndCaching()); + configuration.comparableResourceVersion()); } InformerEventSource(InformerEventSourceConfiguration configuration, KubernetesClient client) { - this(configuration, client, false); + this(configuration, client, DEFAULT_COMPARABLE_RESOURCE_VERSION); } @SuppressWarnings({"unchecked", "rawtypes"}) private InformerEventSource( InformerEventSourceConfiguration configuration, KubernetesClient client, - boolean parseResourceVersions) { + boolean comparableResourceVersions) { super( configuration.name(), configuration @@ -107,7 +78,7 @@ private InformerEventSource( .map(gvk -> client.genericKubernetesResources(gvk.apiVersion(), gvk.getKind())) .orElseGet(() -> (MixedOperation) client.resources(configuration.getResourceClass())), configuration, - parseResourceVersions); + comparableResourceVersions); // If there is a primary to secondary mapper there is no need for primary to secondary index. primaryToSecondaryMapper = configuration.getPrimaryToSecondaryMapper(); if (useSecondaryToPrimaryIndex()) { @@ -134,9 +105,7 @@ public void onAdd(R newResource) { resourceType().getSimpleName(), newResource.getMetadata().getResourceVersion()); } - primaryToSecondaryIndex.onAddOrUpdate(newResource); - onAddOrUpdate( - Operation.ADD, newResource, null, () -> InformerEventSource.super.onAdd(newResource)); + onAddOrUpdate(Operation.ADD, newResource, null); } @Override @@ -149,16 +118,11 @@ public void onUpdate(R oldObject, R newObject) { newObject.getMetadata().getResourceVersion(), oldObject.getMetadata().getResourceVersion()); } - primaryToSecondaryIndex.onAddOrUpdate(newObject); - onAddOrUpdate( - Operation.UPDATE, - newObject, - oldObject, - () -> InformerEventSource.super.onUpdate(oldObject, newObject)); + onAddOrUpdate(Operation.UPDATE, newObject, oldObject); } @Override - public void onDelete(R resource, boolean b) { + public synchronized void onDelete(R resource, boolean b) { if (log.isDebugEnabled()) { log.debug( "On delete event received for resource id: {} type: {}", @@ -180,68 +144,28 @@ public synchronized void start() { manager().list().forEach(primaryToSecondaryIndex::onAddOrUpdate); } - private synchronized void onAddOrUpdate( - Operation operation, R newObject, R oldObject, Runnable superOnOp) { + private synchronized void onAddOrUpdate(Operation operation, R newObject, R oldObject) { + primaryToSecondaryIndex.onAddOrUpdate(newObject); var resourceID = ResourceID.fromResource(newObject); - if (canSkipEvent(newObject, oldObject, resourceID)) { + if (temporaryResourceCache.onAddOrUpdateEvent(newObject)) { log.debug( "Skipping event propagation for {}, since was a result of a reconcile action. Resource" + " ID: {}", operation, ResourceID.fromResource(newObject)); - superOnOp.run(); + } else if (eventAcceptedByFilter(operation, newObject, oldObject)) { + log.debug( + "Propagating event for {}, resource with same version not result of a reconciliation." + + " Resource ID: {}", + operation, + resourceID); + propagateEvent(newObject); } else { - superOnOp.run(); - if (eventAcceptedByFilter(operation, newObject, oldObject)) { - log.debug( - "Propagating event for {}, resource with same version not result of a reconciliation." - + " Resource ID: {}", - operation, - resourceID); - propagateEvent(newObject); - } else { - log.debug("Event filtered out for operation: {}, resourceID: {}", operation, resourceID); - } + log.debug("Event filtered out for operation: {}, resourceID: {}", operation, resourceID); } } - private boolean canSkipEvent(R newObject, R oldObject, ResourceID resourceID) { - var res = temporaryResourceCache.getResourceFromCache(resourceID); - if (res.isEmpty()) { - return isEventKnownFromAnnotation(newObject, oldObject); - } - boolean resVersionsEqual = - newObject - .getMetadata() - .getResourceVersion() - .equals(res.get().getMetadata().getResourceVersion()); - log.debug( - "Resource found in temporal cache for id: {} resource versions equal: {}", - resourceID, - resVersionsEqual); - return resVersionsEqual - || temporaryResourceCache.isLaterResourceVersion(resourceID, res.get(), newObject); - } - - private boolean isEventKnownFromAnnotation(R newObject, R oldObject) { - String previous = newObject.getMetadata().getAnnotations().get(PREVIOUS_ANNOTATION_KEY); - boolean known = false; - if (previous != null) { - String[] parts = previous.split(","); - if (id.equals(parts[0])) { - if (oldObject == null && parts.length == 1) { - known = true; - } else if (oldObject != null - && parts.length == 2 - && oldObject.getMetadata().getResourceVersion().equals(parts[1])) { - known = true; - } - } - } - return known; - } - private void propagateEvent(R object) { var primaryResourceIdSet = configuration().getSecondaryToPrimaryMapper().toPrimaryResourceIDs(object); @@ -289,23 +213,19 @@ public Set getSecondaryResources(P primary) { } @Override - public synchronized void handleRecentResourceUpdate( + public void handleRecentResourceUpdate( ResourceID resourceID, R resource, R previousVersionOfResource) { - handleRecentCreateOrUpdate(Operation.UPDATE, resource, previousVersionOfResource); + handleRecentCreateOrUpdate(resource); } @Override - public synchronized void handleRecentResourceCreate(ResourceID resourceID, R resource) { - handleRecentCreateOrUpdate(Operation.ADD, resource, null); + public void handleRecentResourceCreate(ResourceID resourceID, R resource) { + handleRecentCreateOrUpdate(resource); } - private void handleRecentCreateOrUpdate(Operation operation, R newResource, R oldResource) { + private void handleRecentCreateOrUpdate(R newResource) { primaryToSecondaryIndex.onAddOrUpdate(newResource); - temporaryResourceCache.putResource( - newResource, - Optional.ofNullable(oldResource) - .map(r -> r.getMetadata().getResourceVersion()) - .orElse(null)); + temporaryResourceCache.putResource(newResource); } private boolean useSecondaryToPrimaryIndex() { @@ -333,22 +253,6 @@ private boolean acceptedByDeleteFilters(R resource, boolean b) { && (genericFilter == null || genericFilter.accept(resource)); } - /** - * Add an annotation to the resource so that the subsequent will be omitted - * - * @param resourceVersion null if there is no prior version - * @param target mutable resource that will be returned - */ - public R addPreviousAnnotation(String resourceVersion, R target) { - target - .getMetadata() - .getAnnotations() - .put( - PREVIOUS_ANNOTATION_KEY, - id + Optional.ofNullable(resourceVersion).map(rv -> "," + rv).orElse("")); - return target; - } - private enum Operation { ADD, UPDATE diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index abd2b6a752..42e06c9d9a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -32,7 +32,7 @@ import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Informable; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; @@ -253,7 +253,7 @@ public String toString() { final var informerConfig = configuration.getInformerConfig(); final var selector = informerConfig.getLabelSelector(); return "InformerManager [" - + ReconcilerUtils.getResourceTypeNameWithVersion(configuration.getResourceClass()) + + ReconcilerUtilsInternal.getResourceTypeNameWithVersion(configuration.getResourceClass()) + "] watching: " + informerConfig.getEffectiveNamespaces(controllerConfiguration) + (selector != null ? " selector: " + selector : ""); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index 2a6c7ef206..60497bc0c9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -35,7 +35,7 @@ import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.informers.cache.Cache; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.health.InformerHealthIndicator; import io.javaoperatorsdk.operator.health.Status; @@ -131,7 +131,7 @@ public void start() throws OperatorException { } } catch (Exception e) { - ReconcilerUtils.handleKubernetesClientException( + ReconcilerUtilsInternal.handleKubernetesClientException( e, HasMetadata.getFullResourceName(informer.getApiTypeClass())); throw new OperatorException( "Couldn't start informer for " + versionedFullResourceName() + " resources", e); @@ -143,7 +143,7 @@ private String versionedFullResourceName() { if (apiTypeClass.isAssignableFrom(GenericKubernetesResource.class)) { return GenericKubernetesResource.class.getSimpleName(); } - return ReconcilerUtils.getResourceTypeNameWithVersion(apiTypeClass); + return ReconcilerUtilsInternal.getResourceTypeNameWithVersion(apiTypeClass); } @Override @@ -156,6 +156,10 @@ public Optional get(ResourceID resourceID) { return Optional.ofNullable(cache.getByKey(getKey(resourceID))); } + public String getLastSyncResourceVersion() { + return this.informer.lastSyncResourceVersion(); + } + private String getKey(ResourceID resourceID) { return Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), resourceID.getName()); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 2679918b60..0ade9a44c0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -22,6 +22,7 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.slf4j.Logger; @@ -34,6 +35,7 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Informable; import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; +import io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils; import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; import io.javaoperatorsdk.operator.health.InformerHealthIndicator; import io.javaoperatorsdk.operator.health.InformerWrappingEventSourceHealthIndicator; @@ -55,7 +57,7 @@ public abstract class ManagedInformerEventSource< private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class); private InformerManager cache; - private final boolean parseResourceVersions; + private final boolean comparableResourceVersions; private ControllerConfiguration controllerConfiguration; private final C configuration; private final Map>> indexers = new HashMap<>(); @@ -63,9 +65,9 @@ public abstract class ManagedInformerEventSource< protected MixedOperation client; protected ManagedInformerEventSource( - String name, MixedOperation client, C configuration, boolean parseResourceVersions) { + String name, MixedOperation client, C configuration, boolean comparableResourceVersions) { super(configuration.getResourceClass(), name); - this.parseResourceVersions = parseResourceVersions; + this.comparableResourceVersions = comparableResourceVersions; this.client = client; this.configuration = configuration; } @@ -96,13 +98,35 @@ public void changeNamespaces(Set namespaces) { } } + public R updateAndCacheResource(R resourceToUpdate, UnaryOperator updateMethod) { + ResourceID id = ResourceID.fromResource(resourceToUpdate); + var updated = updateMethod.apply(resourceToUpdate); + handleRecentResourceUpdate(id, updated, resourceToUpdate); + return updated; + } + + public R eventFilteringUpdateAndCacheResource(R resourceToUpdate, UnaryOperator updateMethod) { + ResourceID id = ResourceID.fromResource(resourceToUpdate); + if (log.isDebugEnabled()) { + log.debug("Update and cache: {}", id); + } + try { + temporaryResourceCache.startModifying(id); + var updated = updateMethod.apply(resourceToUpdate); + handleRecentResourceUpdate(id, updated, resourceToUpdate); + return updated; + } finally { + temporaryResourceCache.doneModifying(id); + } + } + @SuppressWarnings("unchecked") @Override public synchronized void start() { if (isRunning()) { return; } - temporaryResourceCache = new TemporaryResourceCache<>(this, parseResourceVersions); + temporaryResourceCache = new TemporaryResourceCache<>(comparableResourceVersions); this.cache = new InformerManager<>(client, configuration, this); cache.setControllerConfiguration(controllerConfiguration); cache.addIndexers(indexers); @@ -122,30 +146,31 @@ public synchronized void stop() { @Override public void handleRecentResourceUpdate( ResourceID resourceID, R resource, R previousVersionOfResource) { - temporaryResourceCache.putResource( - resource, previousVersionOfResource.getMetadata().getResourceVersion()); + temporaryResourceCache.putResource(resource); } @Override public void handleRecentResourceCreate(ResourceID resourceID, R resource) { - temporaryResourceCache.putAddedResource(resource); + temporaryResourceCache.putResource(resource); } @Override public Optional get(ResourceID resourceID) { + var res = cache.get(resourceID); Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); - if (resource.isPresent()) { - log.debug("Resource found in temporary cache for Resource ID: {}", resourceID); + if (comparableResourceVersions + && resource.isPresent() + && res.filter(r -> ReconcileUtils.compareResourceVersions(r, resource.orElseThrow()) > 0) + .isEmpty()) { + log.debug("Latest resource found in temporary cache for Resource ID: {}", resourceID); return resource; - } else { - log.debug( - "Resource not found in temporary cache reading it from informer cache," - + " for Resource ID: {}", - resourceID); - var res = cache.get(resourceID); - log.debug("Resource found in cache: {} for id: {}", res.isPresent(), resourceID); - return res; } + log.debug( + "Resource not found, or older, in temporary cache. Found in informer cache {}, for" + + " Resource ID: {}", + res.isPresent(), + resourceID); + return res; } @SuppressWarnings("unused") diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 06226ae4ba..d41706d702 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -15,16 +15,16 @@ */ package io.javaoperatorsdk.operator.processing.event.source.informer; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -33,157 +33,152 @@ * a create or update is executed the subsequent getResource operation might not return the * up-to-date resource from informer cache, since it is not received yet. * - *

The idea of the solution is, that since an update (for create is simpler) was done - * successfully, and optimistic locking is in place, there were no other operations between reading - * the resource from the cache and the actual update. So when the new resource is stored in the - * temporal cache only if the informer still has the previous resource version, from before the - * update. If not, that means there were already updates on the cache (either by the actual update - * from DependentResource or other) so the resource does not needs to be cached. Subsequently if - * event received from the informer, it means that the cache of the informer was updated, so it - * already contains a more fresh version of the resource. + *

Since an update (for create is simpler) was done successfully we can temporarily track that + * resource if its version is later than the events we've processed. We then know that we can skip + * all events that have the same resource version or earlier than the tracked resource. Once we + * process an event that has the same resource version or later, then we know the tracked resource + * can be removed. + * + *

In some cases it is possible for the informer to deliver events prior to the attempt to put + * the resource in the temporal cache. The startModifying/doneModifying methods are used to pause + * event delivery to ensure that temporal cache recognizes the put entry as an event that can be + * skipped. + * + *

If comparable resource versions are disabled, then this cache is effectively disabled. * * @param resource to cache. */ public class TemporaryResourceCache { - static class ExpirationCache { - private final LinkedHashMap cache; - private final int ttlMs; - - public ExpirationCache(int maxEntries, int ttlMs) { - this.ttlMs = ttlMs; - this.cache = - new LinkedHashMap<>() { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > maxEntries; - } - }; - } + private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); - public void add(K key) { - clean(); - cache.putIfAbsent(key, System.currentTimeMillis()); - } + private final Map cache = new ConcurrentHashMap<>(); + private final boolean comparableResourceVersions; + private final Map activelyModifying = new ConcurrentHashMap<>(); + private String latestResourceVersion; - public boolean contains(K key) { - clean(); - return cache.get(key) != null; - } + public TemporaryResourceCache(boolean comparableResourceVersions) { + this.comparableResourceVersions = comparableResourceVersions; + } - void clean() { - if (!cache.isEmpty()) { - long currentTimeMillis = System.currentTimeMillis(); - var iter = cache.entrySet().iterator(); - // the order will already be from oldest to newest, clean a fixed number of entries to - // amortize the cost amongst multiple calls - for (int i = 0; i < 10 && iter.hasNext(); i++) { - var entry = iter.next(); - if (currentTimeMillis - entry.getValue() > ttlMs) { - iter.remove(); - } - } - } + public void startModifying(ResourceID id) { + if (!comparableResourceVersions) { + return; } + activelyModifying + .compute( + id, + (ignored, lock) -> { + if (lock != null) { + throw new IllegalStateException(); // concurrent modifications to the same resource + // not allowed - this could be relaxed if needed + } + return new ReentrantLock(); + }) + .lock(); } - private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); - - private final Map cache = new ConcurrentHashMap<>(); - - // keep up to the last million deletions for up to 10 minutes - private final ExpirationCache tombstones = new ExpirationCache<>(1000000, 1200000); - private final ManagedInformerEventSource managedInformerEventSource; - private final boolean parseResourceVersions; - - public TemporaryResourceCache( - ManagedInformerEventSource managedInformerEventSource, - boolean parseResourceVersions) { - this.managedInformerEventSource = managedInformerEventSource; - this.parseResourceVersions = parseResourceVersions; + public void doneModifying(ResourceID id) { + if (!comparableResourceVersions) { + return; + } + activelyModifying.computeIfPresent( + id, + (ignored, lock) -> { + lock.unlock(); + return null; + }); } - public synchronized void onDeleteEvent(T resource, boolean unknownState) { - tombstones.add(resource.getMetadata().getUid()); + public void onDeleteEvent(T resource, boolean unknownState) { onEvent(resource, unknownState); } - public synchronized void onAddOrUpdateEvent(T resource) { - onEvent(resource, false); + /** + * @return true if the resourceVersion was already known + */ + public boolean onAddOrUpdateEvent(T resource) { + return onEvent(resource, false); } - synchronized void onEvent(T resource, boolean unknownState) { - cache.computeIfPresent( - ResourceID.fromResource(resource), - (id, cached) -> - (unknownState || !isLaterResourceVersion(id, cached, resource)) ? null : cached); + private boolean onEvent(T resource, boolean unknownState) { + ReentrantLock lock = activelyModifying.get(ResourceID.fromResource(resource)); + if (lock != null) { + lock.lock(); // wait for the modification to finish + lock.unlock(); // simply unlock as the event is guaranteed after the modification + } + boolean[] known = new boolean[1]; + synchronized (this) { + if (!unknownState) { + latestResourceVersion = resource.getMetadata().getResourceVersion(); + } + cache.computeIfPresent( + ResourceID.fromResource(resource), + (id, cached) -> { + boolean remove = unknownState; + if (!unknownState) { + int comp = ReconcileUtils.compareResourceVersions(resource, cached); + if (comp >= 0) { + remove = true; + } + if (comp <= 0) { + known[0] = true; + } + } + if (remove) { + return null; + } + return cached; + }); + return known[0]; + } } - public synchronized void putAddedResource(T newResource) { - putResource(newResource, null); - } + /** put the item into the cache if it's for a later state than what has already been observed. */ + public synchronized void putResource(T newResource) { + if (!comparableResourceVersions) { + return; + } - /** - * put the item into the cache if the previousResourceVersion matches the current state. If not - * the currently cached item is removed. - * - * @param previousResourceVersion null indicates an add - */ - public synchronized void putResource(T newResource, String previousResourceVersion) { var resourceId = ResourceID.fromResource(newResource); - var cachedResource = managedInformerEventSource.get(resourceId).orElse(null); - - boolean moveAhead = false; - if (previousResourceVersion == null && cachedResource == null) { - if (tombstones.contains(newResource.getMetadata().getUid())) { - log.debug( - "Won't resurrect uid {} for resource id: {}", - newResource.getMetadata().getUid(), - resourceId); - return; - } - // we can skip further checks as this is a simple add and there's no previous entry to - // consider - moveAhead = true; + + if (newResource.getMetadata().getResourceVersion() == null) { + log.warn( + "Resource {}: with no resourceVersion put in temporary cache. This is not the expected" + + " usage pattern, only resources returned from the api server should be put in the" + + " cache.", + resourceId); + return; } - if (moveAhead - || (cachedResource != null - && (cachedResource - .getMetadata() - .getResourceVersion() - .equals(previousResourceVersion)) - || isLaterResourceVersion(resourceId, newResource, cachedResource))) { + // check against the latestResourceVersion processed by the TemporaryResourceCache + // If the resource is older, then we can safely ignore. + // + // this also prevents resurrecting recently deleted entities for which the delete event + // has already been processed + if (latestResourceVersion != null + && ReconcileUtils.compareResourceVersions( + latestResourceVersion, newResource.getMetadata().getResourceVersion()) + > 0) { log.debug( - "Temporarily moving ahead to target version {} for resource id: {}", + "Resource {}: resourceVersion {} is not later than latest {}", + resourceId, newResource.getMetadata().getResourceVersion(), - resourceId); - cache.put(resourceId, newResource); - } else if (cache.remove(resourceId) != null) { - log.debug("Removed an obsolete resource from cache for id: {}", resourceId); + latestResourceVersion); + return; } - } - /** - * @return true if {@link ConfigurationService#parseResourceVersionsForEventFilteringAndCaching()} - * is enabled and the resourceVersion of newResource is numerically greater than - * cachedResource, otherwise false - */ - public boolean isLaterResourceVersion(ResourceID resourceId, T newResource, T cachedResource) { - try { - if (parseResourceVersions - && Long.parseLong(newResource.getMetadata().getResourceVersion()) - > Long.parseLong(cachedResource.getMetadata().getResourceVersion())) { - return true; - } - } catch (NumberFormatException e) { + // also make sure that we're later than the existing temporary entry + var cachedResource = getResourceFromCache(resourceId).orElse(null); + + if (cachedResource == null + || ReconcileUtils.compareResourceVersions(newResource, cachedResource) > 0) { log.debug( - "Could not compare resourceVersions {} and {} for {}", + "Temporarily moving ahead to target version {} for resource id: {}", newResource.getMetadata().getResourceVersion(), - cachedResource.getMetadata().getResourceVersion(), resourceId); + cache.put(resourceId, newResource); } - return false; } public synchronized Optional getResourceFromCache(ResourceID resourceID) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java index c87c986f99..e5dae6ca80 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorIT.java @@ -45,7 +45,7 @@ void shouldBePossibleToRetrieveNumberOfRegisteredControllers() { void shouldBePossibleToRetrieveRegisteredControllerByName() { final var operator = new Operator(); final var reconciler = new FooReconciler(); - final var name = ReconcilerUtils.getNameFor(reconciler); + final var name = ReconcilerUtilsInternal.getNameFor(reconciler); var registeredControllers = operator.getRegisteredControllers(); assertTrue(operator.getRegisteredController(name).isEmpty()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternalTest.java similarity index 84% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternalTest.java index 3bbe2a894b..12e45b9c23 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternalTest.java @@ -32,17 +32,17 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultFinalizerName; -import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultNameFor; -import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultReconcilerName; -import static io.javaoperatorsdk.operator.ReconcilerUtils.handleKubernetesClientException; -import static io.javaoperatorsdk.operator.ReconcilerUtils.isFinalizerValid; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.getDefaultFinalizerName; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.getDefaultNameFor; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.getDefaultReconcilerName; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.handleKubernetesClientException; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.isFinalizerValid; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -class ReconcilerUtilsTest { +class ReconcilerUtilsInternalTest { public static final String RESOURCE_URI = "https://kubernetes.docker.internal:6443/apis/tomcatoperator.io/v1/tomcats"; @@ -71,7 +71,7 @@ void equalsSpecObject() { var d1 = createTestDeployment(); var d2 = createTestDeployment(); - assertThat(ReconcilerUtils.specsEqual(d1, d2)).isTrue(); + assertThat(ReconcilerUtilsInternal.specsEqual(d1, d2)).isTrue(); } @Test @@ -80,7 +80,7 @@ void equalArbitraryDifferentSpecsOfObjects() { var d2 = createTestDeployment(); d2.getSpec().getTemplate().getSpec().setHostname("otherhost"); - assertThat(ReconcilerUtils.specsEqual(d1, d2)).isFalse(); + assertThat(ReconcilerUtilsInternal.specsEqual(d1, d2)).isFalse(); } @Test @@ -89,7 +89,7 @@ void getsSpecWithReflection() { deployment.setSpec(new DeploymentSpec()); deployment.getSpec().setReplicas(5); - DeploymentSpec spec = (DeploymentSpec) ReconcilerUtils.getSpec(deployment); + DeploymentSpec spec = (DeploymentSpec) ReconcilerUtilsInternal.getSpec(deployment); assertThat(spec.getReplicas()).isEqualTo(5); } @@ -97,10 +97,10 @@ void getsSpecWithReflection() { void properlyHandlesNullSpec() { Namespace ns = new Namespace(); - final var spec = ReconcilerUtils.getSpec(ns); + final var spec = ReconcilerUtilsInternal.getSpec(ns); assertThat(spec).isNull(); - ReconcilerUtils.setSpec(ns, null); + ReconcilerUtilsInternal.setSpec(ns, null); } @Test @@ -111,7 +111,7 @@ void setsSpecWithReflection() { DeploymentSpec newSpec = new DeploymentSpec(); newSpec.setReplicas(1); - ReconcilerUtils.setSpec(deployment, newSpec); + ReconcilerUtilsInternal.setSpec(deployment, newSpec); assertThat(deployment.getSpec().getReplicas()).isEqualTo(1); } @@ -124,7 +124,7 @@ void setsSpecCustomResourceWithReflection() { TomcatSpec newSpec = new TomcatSpec(); newSpec.setReplicas(1); - ReconcilerUtils.setSpec(tomcat, newSpec); + ReconcilerUtilsInternal.setSpec(tomcat, newSpec); assertThat(tomcat.getSpec().getReplicas()).isEqualTo(1); } @@ -132,7 +132,7 @@ void setsSpecCustomResourceWithReflection() { @Test void loadYamlAsBuilder() { DeploymentBuilder builder = - ReconcilerUtils.loadYaml(DeploymentBuilder.class, getClass(), "deployment.yaml"); + ReconcilerUtilsInternal.loadYaml(DeploymentBuilder.class, getClass(), "deployment.yaml"); builder.accept(ContainerBuilder.class, c -> c.withImage("my-image")); Deployment deployment = builder.editMetadata().withName("my-deployment").and().build(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java new file mode 100644 index 0000000000..675de44a4c --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java @@ -0,0 +1,79 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.api.reconciler; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils.compareResourceVersions; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class ReconcileUtilsTest { + + private static final Logger log = LoggerFactory.getLogger(ReconcileUtilsTest.class); + + @Test + public void compareResourceVersionsTest() { + assertThat(compareResourceVersions("11", "22")).isNegative(); + assertThat(compareResourceVersions("22", "11")).isPositive(); + assertThat(compareResourceVersions("1", "1")).isZero(); + assertThat(compareResourceVersions("11", "11")).isZero(); + assertThat(compareResourceVersions("123", "2")).isPositive(); + assertThat(compareResourceVersions("3", "211")).isNegative(); + + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("aa", "22")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("11", "ba")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("", "22")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("11", "")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("01", "123")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("123", "01")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("3213", "123a")); + assertThrows( + NonComparableResourceVersionException.class, () -> compareResourceVersions("321", "123a")); + } + + // naive performance test that compares the work case scenario for the parsing and non-parsing + // variants + @Test + @Disabled + public void compareResourcePerformanceTest() { + var execNum = 30000000; + var startTime = System.currentTimeMillis(); + for (int i = 0; i < execNum; i++) { + var res = compareResourceVersions("123456788", "123456789"); + } + var dur1 = System.currentTimeMillis() - startTime; + log.info("Duration without parsing: {}", dur1); + startTime = System.currentTimeMillis(); + for (int i = 0; i < execNum; i++) { + var res = Long.parseLong("123456788") > Long.parseLong("123456789"); + } + var dur2 = System.currentTimeMillis() - startTime; + log.info("Duration with parsing: {}", dur2); + + assertThat(dur1).isLessThan(dur2); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java index bb9d6cf71e..1db69a1f9e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java @@ -21,8 +21,10 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.junit.jupiter.api.Assertions.*; @@ -31,6 +33,13 @@ class AbstractDependentResourceTest { + private static final TestCustomResource PRIMARY = new TestCustomResource(); + private static final DefaultContext CONTEXT = createContext(PRIMARY); + + private static DefaultContext createContext(TestCustomResource primary) { + return new DefaultContext<>(mock(), mock(), primary, false, false); + } + @Test void throwsExceptionIfDesiredIsNullOnCreate() { TestDependentResource testDependentResource = new TestDependentResource(); @@ -38,8 +47,7 @@ void throwsExceptionIfDesiredIsNullOnCreate() { testDependentResource.setDesired(null); assertThrows( - DependentResourceException.class, - () -> testDependentResource.reconcile(new TestCustomResource(), null)); + DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT)); } @Test @@ -49,8 +57,7 @@ void throwsExceptionIfDesiredIsNullOnUpdate() { testDependentResource.setDesired(null); assertThrows( - DependentResourceException.class, - () -> testDependentResource.reconcile(new TestCustomResource(), null)); + DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT)); } @Test @@ -60,8 +67,7 @@ void throwsExceptionIfCreateReturnsNull() { testDependentResource.setDesired(configMap()); assertThrows( - DependentResourceException.class, - () -> testDependentResource.reconcile(new TestCustomResource(), null)); + DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT)); } @Test @@ -71,8 +77,28 @@ void throwsExceptionIfUpdateReturnsNull() { testDependentResource.setDesired(configMap()); assertThrows( - DependentResourceException.class, - () -> testDependentResource.reconcile(new TestCustomResource(), null)); + DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT)); + } + + @Test + void checkThatDesiredIsOnlyCalledOnce() { + final var testDependentResource = new DesiredCallCountCheckingDR(); + final var primary = new TestCustomResource(); + final var spec = primary.getSpec(); + spec.setConfigMapName("foo"); + spec.setKey("key"); + spec.setValue("value"); + final var context = createContext(primary); + testDependentResource.reconcile(primary, context); + + spec.setValue("value2"); + testDependentResource.reconcile(primary, context); + + assertEquals(1, testDependentResource.desiredCallCount); + + context.getOrComputeDesiredStateFor( + testDependentResource, p -> testDependentResource.desired(p, context)); + assertEquals(1, testDependentResource.desiredCallCount); } private ConfigMap configMap() { @@ -130,22 +156,12 @@ protected ConfigMap desired(TestCustomResource primary, Context match( return result; } } + + private static class DesiredCallCountCheckingDR extends TestDependentResource { + private short desiredCallCount; + + @Override + public ConfigMap update( + ConfigMap actual, + ConfigMap desired, + TestCustomResource primary, + Context context) { + return desired; + } + + @Override + public ConfigMap create( + ConfigMap desired, TestCustomResource primary, Context context) { + return desired; + } + + @Override + protected ConfigMap desired(TestCustomResource primary, Context context) { + final var spec = primary.getSpec(); + desiredCallCount++; + return new ConfigMapBuilder() + .editOrNewMetadata() + .withName(spec.getConfigMapName()) + .endMetadata() + .addToData(spec.getKey(), spec.getValue()) + .build(); + } + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 495fe98416..8dd7283fb9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -18,37 +18,48 @@ import java.util.Map; import java.util.Optional; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.api.model.apps.DeploymentStatusBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.MockKubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesResourceMatcher.match; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; @SuppressWarnings({"unchecked"}) class GenericKubernetesResourceMatcherTest { - private static final Context context = mock(Context.class); + private static final Context context = new TestContext(); + + private static class TestContext extends DefaultContext { + private final KubernetesClient client = MockKubernetesClient.client(HasMetadata.class); + + public TestContext() { + this(null); + } + + public TestContext(HasMetadata primary) { + super(mock(), mock(), primary, false, false); + } + + @Override + public KubernetesClient getClient() { + return client; + } + } Deployment actual = createDeployment(); Deployment desired = createDeployment(); TestDependentResource dependentResource = new TestDependentResource(desired); - @BeforeAll - static void setUp() { - final var client = MockKubernetesClient.client(HasMetadata.class); - when(context.getClient()).thenReturn(client); - } - @Test void matchesTrivialCases() { assertThat(GenericKubernetesResourceMatcher.match(desired, actual, context).matched()).isTrue(); @@ -77,9 +88,10 @@ void matchesWithStrongSpecEquality() { @Test void doesNotMatchRemovedValues() { actual = createDeployment(); + final var localContext = new TestContext(createPrimary("removed")); assertThat( GenericKubernetesResourceMatcher.match( - dependentResource.desired(createPrimary("removed"), null), actual, context) + dependentResource.getOrComputeDesired(localContext), actual, localContext) .matched()) .withFailMessage("Removing values in metadata should lead to a mismatch") .isFalse(); @@ -186,7 +198,7 @@ ConfigMap createConfigMap() { } Deployment createDeployment() { - return ReconcilerUtils.loadYaml( + return ReconcilerUtilsInternal.loadYaml( Deployment.class, GenericKubernetesResourceMatcherTest.class, "nginx-deployment.yaml"); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterTest.java index 3b6580c5d3..70d664f652 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterTest.java @@ -25,7 +25,7 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.MockKubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -131,7 +131,7 @@ void checkServiceAccount() { } Deployment createDeployment() { - return ReconcilerUtils.loadYaml( + return ReconcilerUtilsInternal.loadYaml( Deployment.class, GenericResourceUpdaterTest.class, "nginx-deployment.yaml"); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java index bbcfa704b5..c4d2f2c77d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcherTest.java @@ -32,7 +32,7 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -419,7 +419,7 @@ void testSortListItems() { } private static R loadResource(String fileName, Class clazz) { - return ReconcilerUtils.loadYaml( + return ReconcilerUtilsInternal.loadYaml( clazz, SSABasedGenericKubernetesResourceMatcherTest.class, fileName); } 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 b0ff06d6b0..460496890a 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 @@ -23,9 +23,11 @@ import java.util.function.Supplier; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; import org.mockito.stubbing.Answer; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -46,6 +48,7 @@ import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @@ -56,10 +59,8 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.TestUtils.markForDeletion; -import static io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.MAX_UPDATE_RETRY; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*; @SuppressWarnings({"unchecked", "rawtypes"}) @@ -154,28 +155,26 @@ public boolean useFinalizer() { @Test void addFinalizerOnNewResource() { - assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(reconciler, never()).reconcile(ArgumentMatchers.eq(testCustomResource), any()); - verify(customResourceFacade, times(1)) - .patchResourceWithSSA( - argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER))); + try (MockedStatic mockedReconcileUtils = mockStatic(ReconcileUtils.class)) { + assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + verify(reconciler, never()).reconcile(ArgumentMatchers.eq(testCustomResource), any()); + mockedReconcileUtils.verify(() -> ReconcileUtils.addFinalizerWithSSA(any()), times(1)); + } } @Test void addFinalizerOnNewResourceWithoutSSA() { - initConfigService(false); - final ReconciliationDispatcher dispatcher = - init(testCustomResource, reconciler, null, customResourceFacade, true); - - assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); - dispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(reconciler, never()).reconcile(ArgumentMatchers.eq(testCustomResource), any()); - verify(customResourceFacade, times(1)) - .patchResource( - argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER)), - any()); - assertThat(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)).isTrue(); + try (MockedStatic mockedReconcileUtils = mockStatic(ReconcileUtils.class)) { + initConfigService(false, false); + final ReconciliationDispatcher dispatcher = + init(testCustomResource, reconciler, null, customResourceFacade, true); + + assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); + dispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + verify(reconciler, never()).reconcile(ArgumentMatchers.eq(testCustomResource), any()); + mockedReconcileUtils.verify(() -> ReconcileUtils.addFinalizer(any()), times(1)); + } } @Test @@ -190,13 +189,13 @@ void patchesBothResourceAndStatusIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.patchResourceAndStatus(testCustomResource); - when(customResourceFacade.patchResource(eq(testCustomResource), any())) + when(customResourceFacade.patchResource(any(), eq(testCustomResource), any())) .thenReturn(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, times(1)).patchResource(eq(testCustomResource), any()); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchResource(any(), eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); } @Test @@ -207,8 +206,8 @@ void patchesStatus() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); - verify(customResourceFacade, never()).patchResource(any(), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); + verify(customResourceFacade, never()).patchResource(any(), any(), any()); } @Test @@ -232,16 +231,19 @@ void callsDeleteIfObjectHasFinalizerAndMarkedForDelete() { @Test void removesDefaultFinalizerOnDeleteIfSet() { - testCustomResource.addFinalizer(DEFAULT_FINALIZER); - markForDeletion(testCustomResource); + try (MockedStatic mockedReconcileUtils = mockStatic(ReconcileUtils.class)) { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + markForDeletion(testCustomResource); - var postExecControl = - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + var postExecControl = + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - assertThat(postExecControl.isFinalizerRemoved()).isTrue(); - verify(customResourceFacade, times(1)).patchResourceWithoutSSA(eq(testCustomResource), any()); + assertThat(postExecControl.isFinalizerRemoved()).isTrue(); + mockedReconcileUtils.verify(() -> ReconcileUtils.removeFinalizer(any()), times(1)); + } } + @Disabled("todo move to ReconcileUtils test") @Test void retriesFinalizerRemovalWithFreshResource() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); @@ -261,6 +263,8 @@ void retriesFinalizerRemovalWithFreshResource() { verify(customResourceFacade, times(1)).getResource(any(), any()); } + // TODO move to utils test + @Disabled @Test void nullResourceIsGracefullyHandledOnFinalizerRemovalRetry() { // simulate the operator not able or not be allowed to get the custom resource during the retry @@ -279,41 +283,6 @@ void nullResourceIsGracefullyHandledOnFinalizerRemovalRetry() { verify(customResourceFacade, times(1)).getResource(any(), any()); } - @Test - void throwsExceptionIfFinalizerRemovalRetryExceeded() { - testCustomResource.addFinalizer(DEFAULT_FINALIZER); - markForDeletion(testCustomResource); - when(customResourceFacade.patchResourceWithoutSSA(any(), any())) - .thenThrow(new KubernetesClientException(null, 409, null)); - when(customResourceFacade.getResource(any(), any())) - .thenAnswer((Answer) invocationOnMock -> createResourceWithFinalizer()); - - var postExecControl = - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - - assertThat(postExecControl.isFinalizerRemoved()).isFalse(); - assertThat(postExecControl.getRuntimeException()).isPresent(); - assertThat(postExecControl.getRuntimeException().get()).isInstanceOf(OperatorException.class); - verify(customResourceFacade, times(MAX_UPDATE_RETRY)).patchResourceWithoutSSA(any(), any()); - verify(customResourceFacade, times(MAX_UPDATE_RETRY - 1)).getResource(any(), any()); - } - - @Test - void throwsExceptionIfFinalizerRemovalClientExceptionIsNotConflict() { - testCustomResource.addFinalizer(DEFAULT_FINALIZER); - markForDeletion(testCustomResource); - when(customResourceFacade.patchResourceWithoutSSA(any(), any())) - .thenThrow(new KubernetesClientException(null, 400, null)); - - var res = - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - - assertThat(res.getRuntimeException()).isPresent(); - assertThat(res.getRuntimeException().get()).isInstanceOf(KubernetesClientException.class); - verify(customResourceFacade, times(1)).patchResourceWithoutSSA(any(), any()); - verify(customResourceFacade, never()).getResource(any(), any()); - } - @Test void doesNotCallDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { final ReconciliationDispatcher dispatcher = @@ -354,7 +323,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); - verify(customResourceFacade, never()).patchResource(any(), any()); + verify(customResourceFacade, never()).patchResource(any(), any(), any()); } @Test @@ -364,23 +333,25 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, never()).patchResource(any(), any()); - verify(customResourceFacade, never()).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, never()).patchResource(any(), any(), any()); + verify(customResourceFacade, never()).patchStatus(any(), eq(testCustomResource), any()); } @Test void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { - removeFinalizers(testCustomResource); - reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); - when(customResourceFacade.patchResourceWithSSA(any())).thenReturn(testCustomResource); - - var postExecControl = - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - - verify(customResourceFacade, times(1)) - .patchResourceWithSSA(argThat(a -> !a.getMetadata().getFinalizers().isEmpty())); - assertThat(postExecControl.updateIsStatusPatch()).isFalse(); - assertThat(postExecControl.getUpdatedCustomResource()).isPresent(); + try (MockedStatic mockedReconcileUtils = mockStatic(ReconcileUtils.class)) { + removeFinalizers(testCustomResource); + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); + mockedReconcileUtils + .when(() -> ReconcileUtils.addFinalizerWithSSA(any())) + .thenReturn(testCustomResource); + var postExecControl = + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + + mockedReconcileUtils.verify(() -> ReconcileUtils.addFinalizerWithSSA(any()), times(1)); + assertThat(postExecControl.updateIsStatusPatch()).isFalse(); + assertThat(postExecControl.getUpdatedCustomResource()).isPresent(); + } } @Test @@ -390,7 +361,7 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, never()).patchResource(any(), any()); + verify(customResourceFacade, never()).patchResource(any(), any(), any()); verify(reconciler, never()).cleanup(eq(testCustomResource), any()); } @@ -471,7 +442,7 @@ void doesNotUpdatesObservedGenerationIfStatusIsNotPatchedWhenUsingSSA() throws E CustomResourceFacade facade = mock(CustomResourceFacade.class); when(config.isGenerationAware()).thenReturn(true); when(reconciler.reconcile(any(), any())).thenReturn(UpdateControl.noUpdate()); - when(facade.patchStatus(any(), any())).thenReturn(observedGenResource); + when(facade.patchStatus(any(), any(), any())).thenReturn(observedGenResource); var dispatcher = init(observedGenResource, reconciler, config, facade, true); PostExecutionControl control = @@ -489,12 +460,12 @@ void doesNotPatchObservedGenerationOnCustomResourcePatch() throws Exception { when(config.isGenerationAware()).thenReturn(true); when(reconciler.reconcile(any(), any())) .thenReturn(UpdateControl.patchResource(observedGenResource)); - when(facade.patchResource(any(), any())).thenReturn(observedGenResource); + when(facade.patchResource(any(), any(), any())).thenReturn(observedGenResource); var dispatcher = init(observedGenResource, reconciler, config, facade, false); dispatcher.handleExecution(executionScopeWithCREvent(observedGenResource)); - verify(facade, never()).patchStatus(any(), any()); + verify(facade, never()).patchStatus(any(), any(), any()); } @Test @@ -529,7 +500,7 @@ public boolean isLastAttempt() { false) .setResource(testCustomResource)); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); } @@ -550,7 +521,7 @@ void callErrorStatusHandlerEvenOnFirstError() { var postExecControl = reconciliationDispatcher.handleExecution( new ExecutionScope(null, null, false, false).setResource(testCustomResource)); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); assertThat(postExecControl.exceptionDuringExecution()).isTrue(); } @@ -573,7 +544,7 @@ void errorHandlerCanInstructNoRetryWithUpdate() { new ExecutionScope(null, null, false, false).setResource(testCustomResource)); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); assertThat(postExecControl.exceptionDuringExecution()).isFalse(); } @@ -595,7 +566,7 @@ void errorHandlerCanInstructNoRetryNoUpdate() { new ExecutionScope(null, null, false, false).setResource(testCustomResource)); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); - verify(customResourceFacade, times(0)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(0)).patchStatus(any(), eq(testCustomResource), any()); assertThat(postExecControl.exceptionDuringExecution()).isFalse(); } @@ -611,7 +582,7 @@ void errorStatusHandlerCanPatchResource() { reconciliationDispatcher.handleExecution( new ExecutionScope(null, null, false, false).setResource(testCustomResource)); - verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); + verify(customResourceFacade, times(1)).patchStatus(any(), eq(testCustomResource), any()); verify(reconciler, times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); } @@ -660,6 +631,7 @@ void canSkipSchedulingMaxDelayIf() { } @Test + @Disabled("Move to reconciler utils test") void retriesAddingFinalizerWithoutSSA() { initConfigService(false); reconciliationDispatcher = @@ -667,7 +639,7 @@ void retriesAddingFinalizerWithoutSSA() { removeFinalizers(testCustomResource); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); - when(customResourceFacade.patchResource(any(), any())) + when(customResourceFacade.patchResourceWithoutSSA(any(), any())) .thenThrow(new KubernetesClientException(null, 409, null)) .thenReturn(testCustomResource); when(customResourceFacade.getResource(any(), any())) @@ -680,7 +652,7 @@ void retriesAddingFinalizerWithoutSSA() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(customResourceFacade, times(2)).patchResource(any(), any()); + verify(customResourceFacade, times(2)).patchResourceWithoutSSA(any(), any()); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java index dcd10b4225..14a4526698 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java @@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.MockKubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; @@ -46,7 +46,7 @@ class ControllerEventSourceTest extends AbstractEventSourceTestBase, EventHandler> { public static final String FINALIZER = - ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class); + ReconcilerUtilsInternal.getDefaultFinalizerName(TestCustomResource.class); private final TestController testController = new TestController(true); private final ControllerConfiguration controllerConfig = mock(ControllerConfiguration.class); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 208d6aeaaa..f54e47304b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -94,31 +94,18 @@ public synchronized void start() {} } @Test - void skipsEventPropagationIfResourceWithSameVersionInResourceCache() { + void skipsEventPropagation() { when(temporaryResourceCacheMock.getResourceFromCache(any())) .thenReturn(Optional.of(testDeployment())); + when(temporaryResourceCacheMock.onAddOrUpdateEvent(any())).thenReturn(true); + informerEventSource.onAdd(testDeployment()); informerEventSource.onUpdate(testDeployment(), testDeployment()); verify(eventHandlerMock, never()).handleEvent(any()); } - @Test - void skipsAddEventPropagationViaAnnotation() { - informerEventSource.onAdd(informerEventSource.addPreviousAnnotation(null, testDeployment())); - - verify(eventHandlerMock, never()).handleEvent(any()); - } - - @Test - void skipsUpdateEventPropagationViaAnnotation() { - informerEventSource.onUpdate( - testDeployment(), informerEventSource.addPreviousAnnotation("1", testDeployment())); - - verify(eventHandlerMock, never()).handleEvent(any()); - } - @Test void processEventPropagationWithoutAnnotation() { informerEventSource.onUpdate(testDeployment(), testDeployment()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index e3dc2c82e4..4b12148015 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -16,10 +16,10 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.Map; -import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,49 +27,40 @@ import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.informer.TemporaryResourceCache.ExpirationCache; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; class TemporaryPrimaryResourceCacheTest { public static final String RESOURCE_VERSION = "2"; - @SuppressWarnings("unchecked") - private InformerEventSource informerEventSource; - private TemporaryResourceCache temporaryResourceCache; @BeforeEach void setup() { - informerEventSource = mock(InformerEventSource.class); - temporaryResourceCache = new TemporaryResourceCache<>(informerEventSource, false); + temporaryResourceCache = new TemporaryResourceCache<>(true); } @Test void updateAddsTheResourceIntoCacheIfTheInformerHasThePreviousResourceVersion() { var testResource = testResource(); var prevTestResource = testResource(); - prevTestResource.getMetadata().setResourceVersion("0"); - when(informerEventSource.get(any())).thenReturn(Optional.of(prevTestResource)); + prevTestResource.getMetadata().setResourceVersion("1"); - temporaryResourceCache.putResource(testResource, "0"); + temporaryResourceCache.putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isPresent(); } @Test - void updateNotAddsTheResourceIntoCacheIfTheInformerHasOtherVersion() { + void updateNotAddsTheResourceIntoCacheIfLaterVersionKnown() { var testResource = testResource(); - var informerCachedResource = testResource(); - informerCachedResource.getMetadata().setResourceVersion("x"); - when(informerEventSource.get(any())).thenReturn(Optional.of(informerCachedResource)); - temporaryResourceCache.putResource(testResource, "0"); + temporaryResourceCache.onAddOrUpdateEvent( + testResource.toBuilder().editMetadata().withResourceVersion("3").endMetadata().build()); + + temporaryResourceCache.putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isNotPresent(); @@ -78,9 +69,8 @@ void updateNotAddsTheResourceIntoCacheIfTheInformerHasOtherVersion() { @Test void addOperationAddsTheResourceIfInformerCacheStillEmpty() { var testResource = testResource(); - when(informerEventSource.get(any())).thenReturn(Optional.empty()); - temporaryResourceCache.putAddedResource(testResource); + temporaryResourceCache.putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isPresent(); @@ -89,46 +79,79 @@ void addOperationAddsTheResourceIfInformerCacheStillEmpty() { @Test void addOperationNotAddsTheResourceIfInformerCacheNotEmpty() { var testResource = testResource(); - when(informerEventSource.get(any())).thenReturn(Optional.of(testResource())); - temporaryResourceCache.putAddedResource(testResource); + temporaryResourceCache.putResource(testResource); + + temporaryResourceCache.putResource( + new ConfigMapBuilder(testResource) + .editMetadata() + .withResourceVersion("1") + .endMetadata() + .build()); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); - assertThat(cached).isNotPresent(); + assertThat(cached.orElseThrow().getMetadata().getResourceVersion()).isEqualTo(RESOURCE_VERSION); } @Test void removesResourceFromCache() { ConfigMap testResource = propagateTestResourceToCache(); - temporaryResourceCache.onAddOrUpdateEvent(testResource()); + temporaryResourceCache.onAddOrUpdateEvent( + new ConfigMapBuilder(testResource) + .editMetadata() + .withResourceVersion("3") + .endMetadata() + .build()); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isNotPresent(); } @Test - void resourceVersionParsing() { - this.temporaryResourceCache = new TemporaryResourceCache<>(informerEventSource, true); + void nonComparableResourceVersionsDisables() { + this.temporaryResourceCache = new TemporaryResourceCache<>(false); - ConfigMap testResource = propagateTestResourceToCache(); + this.temporaryResourceCache.putResource(testResource()); - // an event with a newer version will not remove - temporaryResourceCache.onAddOrUpdateEvent( - new ConfigMapBuilder(testResource) - .editMetadata() - .withResourceVersion("1") - .endMetadata() - .build()); + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource()))) + .isEmpty(); + } - assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) - .isPresent(); + @Test + void lockedEventBeforePut() throws Exception { + var testResource = testResource(); - // anything else will remove - temporaryResourceCache.onAddOrUpdateEvent(testResource()); + temporaryResourceCache.startModifying(ResourceID.fromResource(testResource)); - assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) - .isNotPresent(); + ExecutorService ex = Executors.newSingleThreadExecutor(); + try { + var result = ex.submit(() -> temporaryResourceCache.onAddOrUpdateEvent(testResource)); + + temporaryResourceCache.putResource(testResource); + assertThat(result.isDone()).isFalse(); + temporaryResourceCache.doneModifying(ResourceID.fromResource(testResource)); + assertThat(result.get(10, TimeUnit.SECONDS)).isTrue(); + } finally { + ex.shutdownNow(); + } + } + + @Test + void putBeforeEvent() { + var testResource = testResource(); + + // first ensure an event is not known + var result = temporaryResourceCache.onAddOrUpdateEvent(testResource); + assertThat(result).isFalse(); + + var nextResource = testResource(); + nextResource.getMetadata().setResourceVersion("3"); + temporaryResourceCache.putResource(nextResource); + + // now expect an event with the matching resourceVersion to be known after the put + result = temporaryResourceCache.onAddOrUpdateEvent(nextResource); + assertThat(result).isTrue(); } @Test @@ -143,45 +166,15 @@ void rapidDeletion() { .endMetadata() .build(), false); - temporaryResourceCache.putAddedResource(testResource); + temporaryResourceCache.putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isEmpty(); } - @Test - void expirationCacheMax() { - ExpirationCache cache = new ExpirationCache<>(2, Integer.MAX_VALUE); - - cache.add(1); - cache.add(2); - cache.add(3); - - assertThat(cache.contains(1)).isFalse(); - assertThat(cache.contains(2)).isTrue(); - assertThat(cache.contains(3)).isTrue(); - } - - @Test - void expirationCacheTtl() { - ExpirationCache cache = new ExpirationCache<>(2, 1); - - cache.add(1); - cache.add(2); - - Awaitility.await() - .atMost(1, TimeUnit.SECONDS) - .untilAsserted( - () -> { - assertThat(cache.contains(1)).isFalse(); - assertThat(cache.contains(2)).isFalse(); - }); - } - private ConfigMap propagateTestResourceToCache() { var testResource = testResource(); - when(informerEventSource.get(any())).thenReturn(Optional.empty()); - temporaryResourceCache.putAddedResource(testResource); + temporaryResourceCache.putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isPresent(); return testResource; diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index e3072dd17a..60c235a9ec 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT operator-framework-junit-5 diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java index 0a33293b53..1faa545f54 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java @@ -44,7 +44,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.LocalPortForward; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.RegisteredController; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; @@ -140,7 +140,7 @@ public static Builder builder() { } public static void applyCrd(Class resourceClass, KubernetesClient client) { - applyCrd(ReconcilerUtils.getResourceTypeName(resourceClass), client); + applyCrd(ReconcilerUtilsInternal.getResourceTypeName(resourceClass), client); } /** @@ -192,7 +192,7 @@ private static void applyCrd(String crdString, String path, KubernetesClient cli * @param crClass the custom resource class for which we want to apply the CRD */ public void applyCrd(Class crClass) { - applyCrd(ReconcilerUtils.getResourceTypeName(crClass)); + applyCrd(ReconcilerUtilsInternal.getResourceTypeName(crClass)); } /** @@ -203,7 +203,7 @@ public void applyCrd(Class crClass) { * * @param resourceTypeName the resource type name associated with the CRD to be applied, * typically, given a resource type, its name would be obtained using {@link - * ReconcilerUtils#getResourceTypeName(Class)} + * ReconcilerUtilsInternal#getResourceTypeName(Class)} */ public void applyCrd(String resourceTypeName) { // first attempt to use a manually defined CRD @@ -296,7 +296,7 @@ protected void before(ExtensionContext context) { ref.controllerConfigurationOverrider.accept(oconfig); } - final var resourceTypeName = ReconcilerUtils.getResourceTypeName(resourceClass); + final var resourceTypeName = ReconcilerUtilsInternal.getResourceTypeName(resourceClass); // only try to apply a CRD for the reconciler if it is associated to a CR if (CustomResource.class.isAssignableFrom(resourceClass)) { applyCrd(resourceTypeName); diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 7b4a12824d..253907eb1e 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT operator-framework diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java index db99324ae2..457de54ca3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/LeaderElectionPermissionIT.java @@ -26,7 +26,7 @@ import io.javaoperatorsdk.annotation.Sample; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -87,14 +87,14 @@ public UpdateControl reconcile(ConfigMap resource, Context private void applyRoleBinding() { var clusterRoleBinding = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( RoleBinding.class, this.getClass(), "leader-elector-stop-noaccess-role-binding.yaml"); adminClient.resource(clusterRoleBinding).createOrReplace(); } private void applyRole() { var role = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( Role.class, this.getClass(), "leader-elector-stop-role-noaccess.yaml"); adminClient.resource(role).createOrReplace(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java index 9667c22486..18e076e2bf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/builtinresourcecleaner/BuiltInResourceCleanerIT.java @@ -24,7 +24,7 @@ import io.fabric8.kubernetes.api.model.Service; import io.javaoperatorsdk.annotation.Sample; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.dependent.standalonedependent.StandaloneDependentResourceIT; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; @@ -85,7 +85,7 @@ void cleanerIsCalledOnBuiltInResource() { Service testService() { Service service = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( Service.class, StandaloneDependentResourceIT.class, "/io/javaoperatorsdk/operator/service-template.yaml"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/PreviousAnnotationDisabledIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/ComparableResourceVersionsDisabledIT.java similarity index 94% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/PreviousAnnotationDisabledIT.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/ComparableResourceVersionsDisabledIT.java index 17fe6b7125..6577d4ca59 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/PreviousAnnotationDisabledIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/ComparableResourceVersionsDisabledIT.java @@ -34,9 +34,7 @@ class PreviousAnnotationDisabledIT { @RegisterExtension LocallyRunOperatorExtension operator = LocallyRunOperatorExtension.builder() - .withReconciler(new CreateUpdateEventFilterTestReconciler()) - .withConfigurationService( - overrider -> overrider.withPreviousAnnotationForDependentResources(false)) + .withReconciler(new CreateUpdateEventFilterTestReconciler(false)) .build(); @Test diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java index 40bf2cc350..4344356ff9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java @@ -41,6 +41,16 @@ public class CreateUpdateEventFilterTestReconciler private final DirectConfigMapDependentResource configMapDR = new DirectConfigMapDependentResource(ConfigMap.class); + private final boolean comparableResourceVersion; + + public CreateUpdateEventFilterTestReconciler(boolean comparableResourceVersion) { + this.comparableResourceVersion = comparableResourceVersion; + } + + public CreateUpdateEventFilterTestReconciler() { + this(true); + } + @Override public UpdateControl reconcile( CreateUpdateEventFilterTestCustomResource resource, @@ -89,6 +99,7 @@ public List> prepareEv InformerEventSourceConfiguration.from( ConfigMap.class, CreateUpdateEventFilterTestCustomResource.class) .withLabelSelector("integrationtest = " + this.getClass().getSimpleName()) + .withComparableResourceVersion(comparableResourceVersion) .build(); final var informerEventSource = new InformerEventSource<>(informerConfiguration, context); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientIT.java index 59faaae90b..eb39fa0657 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/infrastructureclient/InfrastructureClientIT.java @@ -28,7 +28,7 @@ import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.KubernetesClientException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -127,23 +127,25 @@ void shouldNotAccessNotPermittedResources() { private void applyClusterRoleBinding(String filename) { var clusterRoleBinding = - ReconcilerUtils.loadYaml(ClusterRoleBinding.class, this.getClass(), filename); + ReconcilerUtilsInternal.loadYaml(ClusterRoleBinding.class, this.getClass(), filename); operator.getInfrastructureKubernetesClient().resource(clusterRoleBinding).serverSideApply(); } private void applyClusterRole(String filename) { - var clusterRole = ReconcilerUtils.loadYaml(ClusterRole.class, this.getClass(), filename); + var clusterRole = + ReconcilerUtilsInternal.loadYaml(ClusterRole.class, this.getClass(), filename); operator.getInfrastructureKubernetesClient().resource(clusterRole).serverSideApply(); } private void removeClusterRoleBinding(String filename) { var clusterRoleBinding = - ReconcilerUtils.loadYaml(ClusterRoleBinding.class, this.getClass(), filename); + ReconcilerUtilsInternal.loadYaml(ClusterRoleBinding.class, this.getClass(), filename); operator.getInfrastructureKubernetesClient().resource(clusterRoleBinding).delete(); } private void removeClusterRole(String filename) { - var clusterRole = ReconcilerUtils.loadYaml(ClusterRole.class, this.getClass(), filename); + var clusterRole = + ReconcilerUtilsInternal.loadYaml(ClusterRole.class, this.getClass(), filename); operator.getInfrastructureKubernetesClient().resource(clusterRole).delete(); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java index e091896597..eb19f9e249 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/patchresourceandstatusnossa/PatchResourceAndStatusNoSSAReconciler.java @@ -45,7 +45,7 @@ public UpdateControl reconcile( Context context) { numberOfExecutions.addAndGet(1); - log.info("Value: " + resource.getSpec().getValue()); + log.info("Value: {}", resource.getSpec().getValue()); if (removeAnnotation) { resource.getMetadata().getAnnotations().remove(TEST_ANNOTATION); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/TestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/TestReconciler.java index b614b97f3a..6bb184d374 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/TestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/simple/TestReconciler.java @@ -25,7 +25,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @@ -38,7 +38,7 @@ public class TestReconciler private static final Logger log = LoggerFactory.getLogger(TestReconciler.class); public static final String FINALIZER_NAME = - ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class); + ReconcilerUtilsInternal.getDefaultFinalizerName(TestCustomResource.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); private final AtomicInteger numberOfCleanupExecutions = new AtomicInteger(0); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java index 370f09509f..ffd0f6b904 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java @@ -29,7 +29,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Service; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.AnnotationConfigurable; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.dependent.ConfigurationConverter; @@ -133,13 +133,13 @@ void missingAnnotationCreatesDefaultConfig() { final var reconciler = new MissingAnnotationReconciler(); var config = configFor(reconciler); - assertThat(config.getName()).isEqualTo(ReconcilerUtils.getNameFor(reconciler)); + assertThat(config.getName()).isEqualTo(ReconcilerUtilsInternal.getNameFor(reconciler)); assertThat(config.getRetry()).isInstanceOf(GenericRetry.class); assertThat(config.getRateLimiter()).isInstanceOf(LinearRateLimiter.class); assertThat(config.maxReconciliationInterval()).hasValue(Duration.ofHours(DEFAULT_INTERVAL)); assertThat(config.fieldManager()).isEqualTo(config.getName()); assertThat(config.getFinalizerName()) - .isEqualTo(ReconcilerUtils.getDefaultFinalizerName(config.getResourceClass())); + .isEqualTo(ReconcilerUtilsInternal.getDefaultFinalizerName(config.getResourceClass())); final var informerConfig = config.getInformerConfig(); assertThat(informerConfig.getLabelSelector()).isNull(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index 1b328ccaf9..fa31575b9e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -20,7 +20,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.Version; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -40,7 +40,7 @@ void returnsValuesFromControllerAnnotationFinalizer() { assertEquals( CustomResource.getCRDName(TestCustomResource.class), configuration.getResourceTypeName()); assertEquals( - ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class), + ReconcilerUtilsInternal.getDefaultFinalizerName(TestCustomResource.class), configuration.getFinalizerName()); assertEquals(TestCustomResource.class, configuration.getResourceClass()); assertFalse(configuration.isGenerationAware()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java index de485cfc4e..5a9d9a7f06 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/ExternalStateReconciler.java @@ -104,13 +104,13 @@ private void createExternalResource( .withData(Map.of(ID_KEY, createdResource.getId())) .build(); configMap.addOwnerReference(resource); - context.getClient().configMaps().resource(configMap).create(); var primaryID = ResourceID.fromResource(resource); // Making sure that the created resources are in the cache for the next reconciliation. // This is critical in this case, since on next reconciliation if it would not be in the cache // it would be created again. - configMapEventSource.handleRecentResourceCreate(primaryID, configMap); + configMapEventSource.eventFilteringUpdateAndCacheResource( + configMap, toCreate -> context.getClient().configMaps().resource(toCreate).create()); externalResourceEventSource.handleRecentResourceCreate(primaryID, createdResource); } @@ -128,6 +128,7 @@ public DeleteControl cleanup( return DeleteControl.defaultDelete(); } + @Override public int getNumberOfExecutions() { return numberOfExecutions.get(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java index 221d7363a3..ce98af58e0 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/informerrelatedbehavior/InformerRelatedBehaviorITS.java @@ -34,7 +34,7 @@ import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.health.InformerHealthIndicator; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource; @@ -399,23 +399,25 @@ private void setFullResourcesAccess() { private void addRoleBindingsToTestNamespaces() { var role = - ReconcilerUtils.loadYaml(Role.class, this.getClass(), "rbac-test-only-main-ns-access.yaml"); + ReconcilerUtilsInternal.loadYaml( + Role.class, this.getClass(), "rbac-test-only-main-ns-access.yaml"); adminClient.resource(role).inNamespace(actualNamespace).createOrReplace(); var roleBinding = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( RoleBinding.class, this.getClass(), "rbac-test-only-main-ns-access-binding.yaml"); adminClient.resource(roleBinding).inNamespace(actualNamespace).createOrReplace(); } private void applyClusterRoleBinding() { var clusterRoleBinding = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( ClusterRoleBinding.class, this.getClass(), "rbac-test-role-binding.yaml"); adminClient.resource(clusterRoleBinding).createOrReplace(); } private void applyClusterRole(String filename) { - var clusterRole = ReconcilerUtils.loadYaml(ClusterRole.class, this.getClass(), filename); + var clusterRole = + ReconcilerUtilsInternal.loadYaml(ClusterRole.class, this.getClass(), filename); adminClient.resource(clusterRole).createOrReplace(); } @@ -431,7 +433,7 @@ private Namespace namespace(String name) { private void removeClusterRoleBinding() { var clusterRoleBinding = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( ClusterRoleBinding.class, this.getClass(), "rbac-test-role-binding.yaml"); adminClient.resource(clusterRoleBinding).delete(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java index 1bb34de16c..fb243251f3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/servicestrictmatcher/ServiceDependentResource.java @@ -26,7 +26,7 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesResourceMatcher; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.loadYaml; @KubernetesDependent public class ServiceDependentResource diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java index 7cd65bd7ef..6a998b3ea4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/ssalegacymatcher/ServiceDependentResource.java @@ -25,7 +25,7 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesResourceMatcher; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.loadYaml; @KubernetesDependent public class ServiceDependentResource diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java index 6f97be1be7..92f033d681 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentTestReconciler.java @@ -20,7 +20,7 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClientException; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; @@ -90,7 +90,7 @@ protected Deployment desired( StandaloneDependentTestCustomResource primary, Context context) { Deployment deployment = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( Deployment.class, StandaloneDependentResourceIT.class, "/io/javaoperatorsdk/operator/nginx-deployment.yaml"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java index e86c772cda..e4bcaac460 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java @@ -17,7 +17,7 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.apps.StatefulSet; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; @@ -32,7 +32,7 @@ protected StatefulSet desired( StatefulSetDesiredSanitizerCustomResource primary, Context context) { var template = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( StatefulSet.class, getClass(), "/io/javaoperatorsdk/operator/statefulset.yaml"); template.setMetadata( new ObjectMetaBuilder() diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseService.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseService.java index 06abcc0889..7a0d50debf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseService.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseService.java @@ -19,7 +19,7 @@ import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.workflow.complexdependent.ComplexWorkflowCustomResource; @@ -33,7 +33,7 @@ public BaseService(String component) { protected Service desired( ComplexWorkflowCustomResource primary, Context context) { var template = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( Service.class, getClass(), "/io/javaoperatorsdk/operator/workflow/complexdependent/service.yaml"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseStatefulSet.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseStatefulSet.java index b0a7b60805..1e4aa73e80 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseStatefulSet.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/complexdependent/dependent/BaseStatefulSet.java @@ -19,7 +19,7 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.workflow.complexdependent.ComplexWorkflowCustomResource; @@ -32,7 +32,7 @@ public BaseStatefulSet(String component) { protected StatefulSet desired( ComplexWorkflowCustomResource primary, Context context) { var template = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( StatefulSet.class, getClass(), "/io/javaoperatorsdk/operator/workflow/complexdependent/statefulset.yaml"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java index b9aa595b76..e5c7f726f5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/workflowallfeature/DeploymentDependentResource.java @@ -16,7 +16,7 @@ package io.javaoperatorsdk.operator.workflow.workflowallfeature; import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDNoGCKubernetesDependentResource; @@ -27,7 +27,7 @@ public class DeploymentDependentResource protected Deployment desired( WorkflowAllFeatureCustomResource primary, Context context) { Deployment deployment = - ReconcilerUtils.loadYaml( + ReconcilerUtilsInternal.loadYaml( Deployment.class, WorkflowAllFeatureIT.class, "/io/javaoperatorsdk/operator/nginx-deployment.yaml"); diff --git a/pom.xml b/pom.xml index 415aa79cbc..67e0f45302 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT pom Operator SDK for Java Java SDK for implementing Kubernetes operators diff --git a/sample-operators/controller-namespace-deletion/pom.xml b/sample-operators/controller-namespace-deletion/pom.xml index b8994fe20b..ddfeebacbc 100644 --- a/sample-operators/controller-namespace-deletion/pom.xml +++ b/sample-operators/controller-namespace-deletion/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-controller-namespace-deletion diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index 316b926fef..70485a2f3e 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-leader-election diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 86eb91bee0..a2334ca8c6 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 820a110afb..6079d3bb71 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index f07760ec28..c9fe8c2d06 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 0347b726ac..c4a47069e2 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -18,7 +18,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.informer.Informer; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; @@ -36,7 +36,7 @@ private static String tomcatImage(Tomcat tomcat) { @Override protected Deployment desired(Tomcat tomcat, Context context) { Deployment deployment = - ReconcilerUtils.loadYaml(Deployment.class, getClass(), "deployment.yaml"); + ReconcilerUtilsInternal.loadYaml(Deployment.class, getClass(), "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); final String tomcatName = tomcatMetadata.getName(); deployment = diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index 72f430528e..bcb0e80026 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -18,7 +18,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.informer.Informer; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; @@ -31,7 +31,8 @@ public class ServiceDependentResource extends CRUDKubernetesDependentResource context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); - return new ServiceBuilder(ReconcilerUtils.loadYaml(Service.class, getClass(), "service.yaml")) + return new ServiceBuilder( + ReconcilerUtilsInternal.loadYaml(Service.class, getClass(), "service.yaml")) .editMetadata() .withName(tomcatMetadata.getName()) .withNamespace(tomcatMetadata.getNamespace()) diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 03d363212c..e25920b7da 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk sample-operators - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT sample-webpage-operator diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java index ab4ed8a337..ecfe66d329 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java @@ -21,7 +21,7 @@ import io.javaoperatorsdk.operator.sample.customresource.WebPage; import io.javaoperatorsdk.operator.sample.customresource.WebPageStatus; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.loadYaml; public class Utils { diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 94b460474f..941a159542 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -27,7 +27,7 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.networking.v1.Ingress; -import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -219,7 +219,8 @@ private boolean match(ConfigMap desiredHtmlConfigMap, ConfigMap existingConfigMa } private Service makeDesiredService(WebPage webPage, String ns, Deployment desiredDeployment) { - Service desiredService = ReconcilerUtils.loadYaml(Service.class, getClass(), "service.yaml"); + Service desiredService = + ReconcilerUtilsInternal.loadYaml(Service.class, getClass(), "service.yaml"); desiredService.getMetadata().setName(serviceName(webPage)); desiredService.getMetadata().setNamespace(ns); desiredService.getMetadata().setLabels(lowLevelLabel()); @@ -233,7 +234,7 @@ private Service makeDesiredService(WebPage webPage, String ns, Deployment desire private Deployment makeDesiredDeployment( WebPage webPage, String deploymentName, String ns, String configMapName) { Deployment desiredDeployment = - ReconcilerUtils.loadYaml(Deployment.class, getClass(), "deployment.yaml"); + ReconcilerUtilsInternal.loadYaml(Deployment.class, getClass(), "deployment.yaml"); desiredDeployment.getMetadata().setName(deploymentName); desiredDeployment.getMetadata().setNamespace(ns); desiredDeployment.getMetadata().setLabels(lowLevelLabel()); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java index 6d1f7cc911..e383633ab1 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/DeploymentDependentResource.java @@ -27,7 +27,7 @@ import io.javaoperatorsdk.operator.sample.Utils; import io.javaoperatorsdk.operator.sample.customresource.WebPage; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.loadYaml; import static io.javaoperatorsdk.operator.sample.Utils.configMapName; import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; import static io.javaoperatorsdk.operator.sample.WebPageManagedDependentsReconciler.SELECTOR; diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java index 3dbc784887..02204d415a 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ServiceDependentResource.java @@ -25,7 +25,7 @@ import io.javaoperatorsdk.operator.sample.Utils; import io.javaoperatorsdk.operator.sample.customresource.WebPage; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.ReconcilerUtilsInternal.loadYaml; import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; import static io.javaoperatorsdk.operator.sample.Utils.serviceName; import static io.javaoperatorsdk.operator.sample.WebPageManagedDependentsReconciler.SELECTOR; diff --git a/test-index-processor/pom.xml b/test-index-processor/pom.xml index 3dad709023..5ea4008f78 100644 --- a/test-index-processor/pom.xml +++ b/test-index-processor/pom.xml @@ -22,7 +22,7 @@ io.javaoperatorsdk java-operator-sdk - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT test-index-processor