dependentResources(
Utils.instantiate(dependent.readyPostcondition(), Condition.class, context),
Utils.instantiate(dependent.reconcilePrecondition(), Condition.class, context),
Utils.instantiate(dependent.deletePostcondition(), Condition.class, context),
+ Utils.instantiate(dependent.activationCondition(), Condition.class, context),
eventSourceName);
specsMap.put(dependentName, spec);
}
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 2422f6ef74..86efb457c5 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
@@ -9,7 +9,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+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.client.Config;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.CustomResource;
@@ -19,6 +21,7 @@
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceFactory;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory;
import static io.javaoperatorsdk.operator.api.config.ExecutorServiceManager.newThreadPoolExecutor;
@@ -335,7 +338,7 @@ default ExecutorServiceManager getExecutorServiceManager() {
* resources are created/updated and match was change to use
* Server-Side
* Apply (SSA) by default.
- *
+ *
* SSA based create/update can be still used with the legacy matching, just overriding the match
* method of Kubernetes Dependent Resource.
*
@@ -345,4 +348,20 @@ default boolean ssaBasedCreateUpdateMatchForDependentResources() {
return true;
}
+ /**
+ * Returns the set of default resources for which Server-Side Apply (SSA) will not be used, even
+ * if it is the default behavior for dependent resources as specified by
+ * {@link #ssaBasedCreateUpdateMatchForDependentResources()}. The exception to this is in the case
+ * where the use of SSA is explicitly enabled on the dependent resource directly using
+ * {@link KubernetesDependent#useSSA()}.
+ *
+ * By default, SSA is disabled for {@link ConfigMap} and {@link Secret} resources.
+ *
+ * @return The set of resource types for which SSA will not be used
+ * @since 4.4.0
+ */
+ default Set> defaultNonSSAResource() {
+ return Set.of(ConfigMap.class, Secret.class);
+ }
+
}
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 ebac41f640..15418aed5e 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
@@ -9,6 +9,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
@@ -35,6 +36,7 @@ public class ConfigurationServiceOverrider {
private Duration cacheSyncTimeout;
private ResourceClassResolver resourceClassResolver;
private Boolean ssaBasedCreateUpdateMatchForDependentResources;
+ private Set> defaultNonSSAResource;
ConfigurationServiceOverrider(ConfigurationService original) {
this.original = original;
@@ -150,6 +152,12 @@ public ConfigurationServiceOverrider withSSABasedCreateUpdateMatchForDependentRe
return this;
}
+ public ConfigurationServiceOverrider withDefaultNonSSAResource(
+ Set> defaultNonSSAResource) {
+ this.defaultNonSSAResource = defaultNonSSAResource;
+ return this;
+ }
+
public ConfigurationService build() {
return new BaseConfigurationService(original.getVersion(), cloner, client) {
@Override
@@ -256,6 +264,12 @@ public boolean ssaBasedCreateUpdateMatchForDependentResources() {
? ssaBasedCreateUpdateMatchForDependentResources
: super.ssaBasedCreateUpdateMatchForDependentResources();
}
+
+ @Override
+ public Set> defaultNonSSAResource() {
+ return defaultNonSSAResource != null ? defaultNonSSAResource
+ : super.defaultNonSSAResource();
+ }
};
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java
index 5a2c322657..0ab72ff165 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java
@@ -3,6 +3,8 @@
import java.time.Duration;
import java.util.Optional;
+import io.fabric8.kubernetes.client.extended.leaderelection.LeaderCallbacks;
+
public class LeaderElectionConfiguration {
public static final Duration LEASE_DURATION_DEFAULT_VALUE = Duration.ofSeconds(15);
@@ -17,13 +19,15 @@ public class LeaderElectionConfiguration {
private final Duration renewDeadline;
private final Duration retryPeriod;
+ private final LeaderCallbacks leaderCallbacks;
+
public LeaderElectionConfiguration(String leaseName, String leaseNamespace, String identity) {
this(
leaseName,
leaseNamespace,
LEASE_DURATION_DEFAULT_VALUE,
RENEW_DEADLINE_DEFAULT_VALUE,
- RETRY_PERIOD_DEFAULT_VALUE, identity);
+ RETRY_PERIOD_DEFAULT_VALUE, identity, null);
}
public LeaderElectionConfiguration(String leaseName, String leaseNamespace) {
@@ -32,7 +36,7 @@ public LeaderElectionConfiguration(String leaseName, String leaseNamespace) {
leaseNamespace,
LEASE_DURATION_DEFAULT_VALUE,
RENEW_DEADLINE_DEFAULT_VALUE,
- RETRY_PERIOD_DEFAULT_VALUE, null);
+ RETRY_PERIOD_DEFAULT_VALUE, null, null);
}
public LeaderElectionConfiguration(String leaseName) {
@@ -41,7 +45,7 @@ public LeaderElectionConfiguration(String leaseName) {
null,
LEASE_DURATION_DEFAULT_VALUE,
RENEW_DEADLINE_DEFAULT_VALUE,
- RETRY_PERIOD_DEFAULT_VALUE, null);
+ RETRY_PERIOD_DEFAULT_VALUE, null, null);
}
public LeaderElectionConfiguration(
@@ -50,7 +54,7 @@ public LeaderElectionConfiguration(
Duration leaseDuration,
Duration renewDeadline,
Duration retryPeriod) {
- this(leaseName, leaseNamespace, leaseDuration, renewDeadline, retryPeriod, null);
+ this(leaseName, leaseNamespace, leaseDuration, renewDeadline, retryPeriod, null, null);
}
public LeaderElectionConfiguration(
@@ -59,13 +63,15 @@ public LeaderElectionConfiguration(
Duration leaseDuration,
Duration renewDeadline,
Duration retryPeriod,
- String identity) {
+ String identity,
+ LeaderCallbacks leaderCallbacks) {
this.leaseName = leaseName;
this.leaseNamespace = leaseNamespace;
this.leaseDuration = leaseDuration;
this.renewDeadline = renewDeadline;
this.retryPeriod = retryPeriod;
this.identity = identity;
+ this.leaderCallbacks = leaderCallbacks;
}
public Optional getLeaseNamespace() {
@@ -91,4 +97,8 @@ public Duration getRetryPeriod() {
public Optional getIdentity() {
return Optional.ofNullable(identity);
}
+
+ public Optional getLeaderCallbacks() {
+ return Optional.ofNullable(leaderCallbacks);
+ }
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfigurationBuilder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfigurationBuilder.java
new file mode 100644
index 0000000000..4b21dd9d2d
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfigurationBuilder.java
@@ -0,0 +1,61 @@
+package io.javaoperatorsdk.operator.api.config;
+
+import java.time.Duration;
+
+import io.fabric8.kubernetes.client.extended.leaderelection.LeaderCallbacks;
+
+import static io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration.*;
+
+public final class LeaderElectionConfigurationBuilder {
+
+ private String leaseName;
+ private String leaseNamespace;
+ private String identity;
+ private Duration leaseDuration = LEASE_DURATION_DEFAULT_VALUE;
+ private Duration renewDeadline = RENEW_DEADLINE_DEFAULT_VALUE;
+ private Duration retryPeriod = RETRY_PERIOD_DEFAULT_VALUE;
+ private LeaderCallbacks leaderCallbacks;
+
+ private LeaderElectionConfigurationBuilder(String leaseName) {
+ this.leaseName = leaseName;
+ }
+
+ public static LeaderElectionConfigurationBuilder aLeaderElectionConfiguration(String leaseName) {
+ return new LeaderElectionConfigurationBuilder(leaseName);
+ }
+
+ public LeaderElectionConfigurationBuilder withLeaseNamespace(String leaseNamespace) {
+ this.leaseNamespace = leaseNamespace;
+ return this;
+ }
+
+ public LeaderElectionConfigurationBuilder withIdentity(String identity) {
+ this.identity = identity;
+ return this;
+ }
+
+ public LeaderElectionConfigurationBuilder withLeaseDuration(Duration leaseDuration) {
+ this.leaseDuration = leaseDuration;
+ return this;
+ }
+
+ public LeaderElectionConfigurationBuilder withRenewDeadline(Duration renewDeadline) {
+ this.renewDeadline = renewDeadline;
+ return this;
+ }
+
+ public LeaderElectionConfigurationBuilder withRetryPeriod(Duration retryPeriod) {
+ this.retryPeriod = retryPeriod;
+ return this;
+ }
+
+ public LeaderElectionConfigurationBuilder withLeaderCallbacks(LeaderCallbacks leaderCallbacks) {
+ this.leaderCallbacks = leaderCallbacks;
+ return this;
+ }
+
+ public LeaderElectionConfiguration build() {
+ return new LeaderElectionConfiguration(leaseName, leaseNamespace, leaseDuration, renewDeadline,
+ retryPeriod, identity, leaderCallbacks);
+ }
+}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java
index 58fd9ace4b..1fcd0709fb 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java
@@ -22,18 +22,21 @@ public class DependentResourceSpec {
private final Condition, ?> deletePostCondition;
+ private final Condition, ?> activationCondition;
+
private final String useEventSourceWithName;
public DependentResourceSpec(Class extends DependentResource> dependentResourceClass,
String name, Set dependsOn, Condition, ?> readyCondition,
Condition, ?> reconcileCondition, Condition, ?> deletePostCondition,
- String useEventSourceWithName) {
+ Condition, ?> activationCondition, String useEventSourceWithName) {
this.dependentResourceClass = dependentResourceClass;
this.name = name;
this.dependsOn = dependsOn;
this.readyCondition = readyCondition;
this.reconcileCondition = reconcileCondition;
this.deletePostCondition = deletePostCondition;
+ this.activationCondition = activationCondition;
this.useEventSourceWithName = useEventSourceWithName;
}
@@ -87,6 +90,11 @@ public Condition getDeletePostCondition() {
return deletePostCondition;
}
+ @SuppressWarnings("rawtypes")
+ public Condition getActivationCondition() {
+ return activationCondition;
+ }
+
public Optional getUseEventSourceWithName() {
return Optional.ofNullable(useEventSourceWithName);
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java
index ac51bb7263..a5cdb85257 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java
@@ -9,7 +9,7 @@ public abstract class BaseControl> {
private Long scheduleDelay = null;
public T rescheduleAfter(long delay) {
- this.scheduleDelay = delay;
+ rescheduleAfter(Duration.ofMillis(delay));
return (T) this;
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java
index 5043ed675c..e157ed5fd7 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java
@@ -10,6 +10,7 @@
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceContext;
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
+import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache;
public interface Context {
@@ -43,4 +44,12 @@ Optional getSecondaryResource(Class expectedType,
* ExecutorService initialized by framework for workflows. Used for workflow standalone mode.
*/
ExecutorService getWorkflowExecutorService();
+
+ /**
+ * Retrieves the primary resource cache.
+ *
+ * @return the {@link IndexerResourceCache} associated with the associated {@link Reconciler} for
+ * this context
+ */
+ IndexedResourceCache getPrimaryCache();
}
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 d6e5cb91ea..2b0f20ef33 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
@@ -40,6 +40,11 @@ public Set getSecondaryResources(Class expectedType) {
return getSecondaryResourcesAsStream(expectedType).collect(Collectors.toSet());
}
+ @Override
+ public IndexedResourceCache getPrimaryCache() {
+ return controller.getEventSourceManager().getControllerResourceEventSource();
+ }
+
@Override
public Stream getSecondaryResourcesAsStream(Class expectedType) {
return controller.getEventSourceManager().getResourceEventSourcesFor(expectedType).stream()
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java
index 6d317161f7..48c0e32946 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java
@@ -1,16 +1,17 @@
package io.javaoperatorsdk.operator.api.reconciler;
+import java.time.Duration;
import java.util.Optional;
import io.fabric8.kubernetes.api.model.HasMetadata;
-public class ErrorStatusUpdateControl {
+public class ErrorStatusUpdateControl
+ extends BaseControl> {
private final P resource;
private final boolean patch;
private boolean noRetry = false;
-
public static ErrorStatusUpdateControl patchStatus(T resource) {
return new ErrorStatusUpdateControl<>(resource, true);
}
@@ -49,4 +50,16 @@ public boolean isNoRetry() {
public boolean isPatch() {
return patch;
}
+
+ /**
+ * If re-scheduled using this method, it is not considered as retry, it effectively cancels retry.
+ *
+ * @param delay for next execution
+ * @return ErrorStatusUpdateControl
+ */
+ @Override
+ public ErrorStatusUpdateControl rescheduleAfter(Duration delay) {
+ withNoRetry();
+ return super.rescheduleAfter(delay);
+ }
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/IndexDiscriminator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/IndexDiscriminator.java
new file mode 100644
index 0000000000..7a27397b26
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/IndexDiscriminator.java
@@ -0,0 +1,50 @@
+package io.javaoperatorsdk.operator.api.reconciler;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+
+/**
+ * Uses a custom index of {@link InformerEventSource} to access the target resource. The index needs
+ * to be explicitly created when the event source is defined. This approach improves the performance
+ * to access the resource.
+ */
+public class IndexDiscriminator
+ implements ResourceDiscriminator {
+
+ private final String indexName;
+ private final String eventSourceName;
+ private final Function keyMapper;
+
+ public IndexDiscriminator(String indexName, Function
keyMapper) {
+ this(indexName, null, keyMapper);
+ }
+
+ public IndexDiscriminator(String indexName, String eventSourceName,
+ Function
keyMapper) {
+ this.indexName = indexName;
+ this.eventSourceName = eventSourceName;
+ this.keyMapper = keyMapper;
+ }
+
+ @Override
+ public Optional distinguish(Class resource,
+ P primary,
+ Context context) {
+
+ InformerEventSource eventSource =
+ (InformerEventSource) context
+ .eventSourceRetriever()
+ .getResourceEventSourceFor(resource, eventSourceName);
+ var resources = eventSource.byIndex(indexName, keyMapper.apply(primary));
+ if (resources.isEmpty()) {
+ return Optional.empty();
+ } else if (resources.size() > 1) {
+ throw new IllegalStateException("More than one resource found");
+ } else {
+ return Optional.of(resources.get(0));
+ }
+ }
+}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/IndexedResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/IndexedResourceCache.java
new file mode 100644
index 0000000000..29ac9c073a
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/IndexedResourceCache.java
@@ -0,0 +1,9 @@
+package io.javaoperatorsdk.operator.api.reconciler;
+
+import java.util.List;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+
+public interface IndexedResourceCache extends ResourceCache {
+ List byIndex(String indexName, String indexKey);
+}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceCache.java
similarity index 76%
rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java
rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceCache.java
index b0b9e88746..130bd23e8d 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceCache.java
@@ -1,9 +1,10 @@
-package io.javaoperatorsdk.operator.processing.event.source;
+package io.javaoperatorsdk.operator.api.reconciler;
import java.util.function.Predicate;
import java.util.stream.Stream;
import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.javaoperatorsdk.operator.processing.event.source.Cache;
@SuppressWarnings("unchecked")
public interface ResourceCache extends Cache {
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceIDMatcherDiscriminator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceIDMatcherDiscriminator.java
index d23459e271..da773fc210 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceIDMatcherDiscriminator.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceIDMatcherDiscriminator.java
@@ -5,21 +5,41 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.Cache;
public class ResourceIDMatcherDiscriminator
implements ResourceDiscriminator {
+
+ private final String eventSourceName;
private final Function mapper;
public ResourceIDMatcherDiscriminator(Function
mapper) {
+ this(null, mapper);
+ }
+
+ public ResourceIDMatcherDiscriminator(String eventSourceName, Function
mapper) {
+ this.eventSourceName = eventSourceName;
this.mapper = mapper;
}
+ @SuppressWarnings("unchecked")
@Override
public Optional distinguish(Class resource, P primary, Context context) {
var resourceID = mapper.apply(primary);
- return context.getSecondaryResourcesAsStream(resource)
- .filter(resourceID::isSameResource)
- .findFirst();
+ if (eventSourceName != null) {
+ return ((Cache) context.eventSourceRetriever().getResourceEventSourceFor(resource,
+ eventSourceName))
+ .get(resourceID);
+ } else {
+ var eventSources = context.eventSourceRetriever().getResourceEventSourcesFor(resource);
+ if (eventSources.size() == 1) {
+ return ((Cache) eventSources.get(0)).get(resourceID);
+ } else {
+ return context.getSecondaryResourcesAsStream(resource)
+ .filter(resourceID::isSameResource)
+ .findFirst();
+ }
+ }
}
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java
index 78e9ee4581..e8084cc6c9 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java
@@ -50,6 +50,8 @@
*/
Class extends Condition> deletePostcondition() default Condition.class;
+ Class extends Condition> activationCondition() default Condition.class;
+
/**
* The list of named dependents that need to be reconciled before this one can be.
*
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java
index affeb96bbf..8230dc4cf9 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java
@@ -44,7 +44,7 @@ public interface DependentResource {
* @param eventSourceContext context of event source initialization
* @return an optional event source
*/
- default Optional> eventSource(
+ default Optional extends ResourceEventSource> eventSource(
EventSourceContext eventSourceContext) {
return Optional.empty();
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationEventFilter.java
deleted file mode 100644
index d48343e57c..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationEventFilter.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.javaoperatorsdk.operator.api.reconciler.dependent;
-
-import io.javaoperatorsdk.operator.processing.event.ResourceID;
-
-public interface RecentOperationEventFilter extends RecentOperationCacheFiller {
-
- void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID, R resource);
-
- void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID);
-
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java
index 972b61cd94..04aa5631cf 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java
@@ -34,7 +34,7 @@ protected AbstractEventSourceHolderDependentResource(Class resourceType) {
this.resourceType = resourceType;
}
- public Optional> eventSource(EventSourceContext context) {
+ public Optional eventSource(EventSourceContext context) {
// some sub-classes (e.g. KubernetesDependentResource) can have their event source created
// before this method is called in the managed case, so only create the event source if it
// hasn't already been set.
@@ -67,9 +67,8 @@ public void resolveEventSource(EventSourceRetriever
eventSourceRetriever) {
* @param context for event sources
* @return event source instance
*/
- @SuppressWarnings("unchecked")
public T initEventSource(EventSourceContext
context) {
- return (T) eventSource(context).orElseThrow();
+ return eventSource(context).orElseThrow();
}
@Override
@@ -96,7 +95,7 @@ protected void applyFilters() {
this.eventSource.setGenericFilter(genericFilter);
}
- public Optional> eventSource() {
+ public Optional eventSource() {
return Optional.ofNullable(eventSource);
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/BooleanWithUndefined.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/BooleanWithUndefined.java
new file mode 100644
index 0000000000..fcf7553a4a
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/BooleanWithUndefined.java
@@ -0,0 +1,19 @@
+package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
+
+/**
+ * A replacement for {@link Boolean}, which can't be used in annotations.
+ */
+public enum BooleanWithUndefined {
+ TRUE, FALSE, UNDEFINED;
+
+ Boolean asBoolean() {
+ switch (this) {
+ case TRUE:
+ return Boolean.TRUE;
+ case FALSE:
+ return Boolean.FALSE;
+ default:
+ return null;
+ }
+ }
+}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredResourceSanitizer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredResourceSanitizer.java
new file mode 100644
index 0000000000..45b781af7a
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredResourceSanitizer.java
@@ -0,0 +1,47 @@
+package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaimStatus;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.apps.StatefulSet;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+
+public class DesiredResourceSanitizer {
+
+ private DesiredResourceSanitizer() {}
+
+ public static void sanitizeDesired(R desired, R actual, P primary,
+ Context context, boolean useSSA) {
+ if (useSSA) {
+ if (desired instanceof StatefulSet) {
+ fillDefaultsOnVolumeClaimTemplate((StatefulSet) desired);
+ }
+ if (desired instanceof Secret) {
+ checkIfStringDataUsed((Secret) desired);
+ }
+ }
+ }
+
+ private static void checkIfStringDataUsed(Secret secret) {
+ if (secret.getStringData() != null && !secret.getStringData().isEmpty()) {
+ throw new IllegalStateException(
+ "There is a known issue using StringData with SSA. Use data instead.");
+ }
+ }
+
+ private static void fillDefaultsOnVolumeClaimTemplate(StatefulSet statefulSet) {
+ if (!statefulSet.getSpec().getVolumeClaimTemplates().isEmpty()) {
+ statefulSet.getSpec().getVolumeClaimTemplates().forEach(t -> {
+ if (t.getSpec().getVolumeMode() == null) {
+ t.getSpec().setVolumeMode("Filesystem");
+ }
+ if (t.getStatus() == null) {
+ t.setStatus(new PersistentVolumeClaimStatus());
+ }
+ if (t.getStatus().getPhase() == null) {
+ t.getStatus().setPhase("pending");
+ }
+ });
+ }
+ }
+}
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 3109880063..98198bf39a 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
@@ -1,11 +1,8 @@
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.HasMetadata;
@@ -20,8 +17,10 @@ public class GenericKubernetesResourceMatcher {
private static final String SPEC = "/spec";
+ private static final String METADATA = "/metadata";
private static final String ADD = "add";
private static final String OP = "op";
+ private static final List IGNORED_FIELDS = List.of("/apiVersion", "/kind", "/status");
public static final String METADATA_LABELS = "/metadata/labels";
public static final String METADATA_ANNOTATIONS = "/metadata/annotations";
@@ -184,76 +183,39 @@ public static Result match(R d
"Equality should be false in case of ignore list provided");
}
- if (considerMetadata) {
- Optional> res =
- matchMetadata(desired, actualResource, labelsAndAnnotationsEquality, context);
- if (res.isPresent()) {
- return res.orElseThrow();
- }
- }
-
- final var matched = matchSpec(actualResource, desired, specEquality, context, ignoreList);
- return Result.computed(matched, desired);
- }
-
- private static boolean matchSpec(R actual, R desired, boolean equality,
- Context> context, List ignoreList) {
final var kubernetesSerialization = context.getClient().getKubernetesSerialization();
var desiredNode = kubernetesSerialization.convertValue(desired, JsonNode.class);
- var actualNode = kubernetesSerialization.convertValue(actual, JsonNode.class);
+ var actualNode = kubernetesSerialization.convertValue(actualResource, JsonNode.class);
var wholeDiffJsonPatch = JsonDiff.asJson(desiredNode, actualNode);
- // reflection will be replaced by this:
- // https://github.com/fabric8io/kubernetes-client/issues/3816
- var specDiffJsonPatch = getDiffsImpactingPathsWithPrefixes(wholeDiffJsonPatch, SPEC);
- // In case of equality is set to true, no diffs are allowed, so we return early if diffs exist
- // On contrary (if equality is false), "add" is allowed for cases when for some
- // resources Kubernetes fills-in values into spec.
- if (equality && !specDiffJsonPatch.isEmpty()) {
- return false;
- }
- if (!equality && !ignoreList.isEmpty()) {
- return allDiffsOnIgnoreList(specDiffJsonPatch, ignoreList);
- } else {
- return allDiffsAreAddOps(specDiffJsonPatch);
+ boolean matched = true;
+ for (int i = 0; i < wholeDiffJsonPatch.size() && matched; i++) {
+ var node = wholeDiffJsonPatch.get(i);
+ if (nodeIsChildOf(node, List.of(SPEC))) {
+ matched = match(specEquality, node, ignoreList);
+ } else if (nodeIsChildOf(node, List.of(METADATA))) {
+ // conditionally consider labels and annotations
+ if (considerMetadata
+ && nodeIsChildOf(node, List.of(METADATA_LABELS, METADATA_ANNOTATIONS))) {
+ matched = match(labelsAndAnnotationsEquality, node, Collections.emptyList());
+ }
+ } else if (!nodeIsChildOf(node, IGNORED_FIELDS)) {
+ matched = match(true, node, ignoreList);
+ }
}
+
+ return Result.computed(matched, desired);
}
- private static boolean allDiffsOnIgnoreList(List metadataJSonDiffs,
- List ignoreList) {
- if (metadataJSonDiffs.isEmpty()) {
+ private static boolean match(boolean equality, JsonNode diff,
+ final List ignoreList) {
+ if (equality) {
return false;
}
- return metadataJSonDiffs.stream().allMatch(n -> nodeIsChildOf(n, ignoreList));
- }
-
- private static Optional> matchMetadata(
- R desired,
- R actualResource,
- boolean labelsAndAnnotationsEquality, Context context) {
- if (labelsAndAnnotationsEquality) {
- final var desiredMetadata = desired.getMetadata();
- final var actualMetadata = actualResource.getMetadata();
-
- final var matched =
- Objects.equals(desiredMetadata.getAnnotations(), actualMetadata.getAnnotations()) &&
- Objects.equals(desiredMetadata.getLabels(), actualMetadata.getLabels());
- if (!matched) {
- return Optional.of(Result.computed(false, desired));
- }
- } else {
- final var objectMapper = context.getClient().getKubernetesSerialization();
- var desiredNode = objectMapper.convertValue(desired, JsonNode.class);
- var actualNode = objectMapper.convertValue(actualResource, JsonNode.class);
- var wholeDiffJsonPatch = JsonDiff.asJson(desiredNode, actualNode);
- var metadataJSonDiffs = getDiffsImpactingPathsWithPrefixes(wholeDiffJsonPatch,
- METADATA_LABELS,
- METADATA_ANNOTATIONS);
- if (!allDiffsAreAddOps(metadataJSonDiffs)) {
- return Optional.of(Result.computed(false, desired));
- }
+ if (!ignoreList.isEmpty()) {
+ return nodeIsChildOf(diff, ignoreList);
}
- return Optional.empty();
+ return ADD.equals(diff.get(OP).asText());
}
static boolean nodeIsChildOf(JsonNode n, List prefixes) {
@@ -265,29 +227,6 @@ static String getPath(JsonNode n) {
return n.get(PATH).asText();
}
- static boolean allDiffsAreAddOps(List metadataJSonDiffs) {
- if (metadataJSonDiffs.isEmpty()) {
- return true;
- }
- return metadataJSonDiffs.stream().allMatch(n -> ADD.equals(n.get(OP).asText()));
- }
-
- public static List getDiffsImpactingPathsWithPrefixes(JsonNode diffJsonPatch,
- String... prefixes) {
- if (prefixes != null && prefixes.length > 0) {
- var res = new ArrayList();
- var prefixList = Arrays.asList(prefixes);
- for (int i = 0; i < diffJsonPatch.size(); i++) {
- var node = diffJsonPatch.get(i);
- if (nodeIsChildOf(node, prefixList)) {
- res.add(node);
- }
- }
- return res;
- }
- return Collections.emptyList();
- }
-
@Deprecated(forRemoval = true)
public static Result match(
KubernetesDependentResource dependentResource, R actualResource, P primary,
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java
index 868ae30dbc..eb4c9cf9b0 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java
@@ -7,7 +7,10 @@
import io.javaoperatorsdk.operator.api.reconciler.Constants;
import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
-import io.javaoperatorsdk.operator.processing.event.source.filter.*;
+import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter;
+import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter;
+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.NO_VALUE_SET;
@@ -22,7 +25,7 @@
* namespace is specified then the controller will monitor the namespaces configured for the
* controller.
*
- * @return the list of namespaces this controller monitors
+ * @return the array of namespaces this controller monitors
*/
String[] namespaces() default {Constants.SAME_AS_CONTROLLER};
@@ -69,4 +72,20 @@
Class extends ResourceDiscriminator> resourceDiscriminator() default ResourceDiscriminator.class;
+ /**
+ * Creates the resource only if did not exist before, this applies only if SSA is used.
+ */
+ boolean createResourceOnlyIfNotExistingWithSSA() default KubernetesDependentResourceConfig.DEFAULT_CREATE_RESOURCE_ONLY_IF_NOT_EXISTING_WITH_SSA;
+
+ /**
+ * Determines whether to use SSA (Server-Side Apply) for this dependent. If SSA is used, the
+ * dependent resource will only be created if it did not exist before. Default value is
+ * {@link BooleanWithUndefined#UNDEFINED}, which specifies that the behavior with respect to SSA
+ * is inherited from the global configuration.
+ *
+ * @return {@code true} if SSA is enabled, {@code false} if SSA is disabled,
+ * {@link BooleanWithUndefined#UNDEFINED} if the SSA behavior should be inherited from the
+ * global configuration
+ */
+ BooleanWithUndefined useSSA() default BooleanWithUndefined.UNDEFINED;
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java
index a9a60f8e0a..7a434aecf1 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java
@@ -14,6 +14,8 @@
import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter;
import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter;
+import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig.DEFAULT_CREATE_RESOURCE_ONLY_IF_NOT_EXISTING_WITH_SSA;
+
public class KubernetesDependentConverter implements
ConfigurationConverter, KubernetesDependentResource> {
@@ -25,11 +27,14 @@ public KubernetesDependentResourceConfig configFrom(KubernetesDependent confi
var namespaces = parentConfiguration.getNamespaces();
var configuredNS = false;
String labelSelector = null;
+ var createResourceOnlyIfNotExistingWithSSA =
+ DEFAULT_CREATE_RESOURCE_ONLY_IF_NOT_EXISTING_WITH_SSA;
OnAddFilter extends HasMetadata> onAddFilter = null;
OnUpdateFilter extends HasMetadata> onUpdateFilter = null;
OnDeleteFilter extends HasMetadata> onDeleteFilter = null;
GenericFilter extends HasMetadata> genericFilter = null;
ResourceDiscriminator, ?> resourceDiscriminator = null;
+ Boolean useSSA = null;
if (configAnnotation != null) {
if (!Arrays.equals(KubernetesDependent.DEFAULT_NAMESPACES, configAnnotation.namespaces())) {
namespaces = Set.of(configAnnotation.namespaces());
@@ -39,9 +44,8 @@ public KubernetesDependentResourceConfig configFrom(KubernetesDependent confi
final var fromAnnotation = configAnnotation.labelSelector();
labelSelector = Constants.NO_VALUE_SET.equals(fromAnnotation) ? null : fromAnnotation;
- final var context =
- Utils.contextFor(parentConfiguration, originatingClass,
- configAnnotation.annotationType());
+ final var context = Utils.contextFor(parentConfiguration, originatingClass,
+ configAnnotation.annotationType());
onAddFilter = Utils.instantiate(configAnnotation.onAddFilter(), OnAddFilter.class, context);
onUpdateFilter =
Utils.instantiate(configAnnotation.onUpdateFilter(), OnUpdateFilter.class, context);
@@ -53,9 +57,13 @@ public KubernetesDependentResourceConfig configFrom(KubernetesDependent confi
resourceDiscriminator =
Utils.instantiate(configAnnotation.resourceDiscriminator(), ResourceDiscriminator.class,
context);
+ createResourceOnlyIfNotExistingWithSSA =
+ configAnnotation.createResourceOnlyIfNotExistingWithSSA();
+ useSSA = configAnnotation.useSSA().asBoolean();
}
return new KubernetesDependentResourceConfig(namespaces, labelSelector, configuredNS,
- resourceDiscriminator, onAddFilter, onUpdateFilter, onDeleteFilter, genericFilter);
+ createResourceOnlyIfNotExistingWithSSA,
+ resourceDiscriminator, useSSA, onAddFilter, onUpdateFilter, onDeleteFilter, genericFilter);
}
}
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 20e83f02bf..22ec5a48f9 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
@@ -1,6 +1,6 @@
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
-import java.util.HashMap;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -8,7 +8,6 @@
import org.slf4j.LoggerFactory;
import io.fabric8.kubernetes.api.model.HasMetadata;
-import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.javaoperatorsdk.operator.OperatorException;
@@ -103,36 +102,20 @@ public void configureWith(InformerEventSource informerEventSource) {
setEventSource(informerEventSource);
}
-
- protected R handleCreate(R desired, P primary, Context context) {
- ResourceID resourceID = ResourceID.fromResource(desired);
- try {
- prepareEventFiltering(desired, resourceID);
- return super.handleCreate(desired, primary, context);
- } catch (RuntimeException e) {
- cleanupAfterEventFiltering(resourceID);
- throw e;
- }
- }
-
- protected R handleUpdate(R actual, R desired, P primary, Context
context) {
- ResourceID resourceID = ResourceID.fromResource(desired);
- try {
- prepareEventFiltering(desired, resourceID);
- return super.handleUpdate(actual, desired, primary, context);
- } catch (RuntimeException e) {
- cleanupAfterEventFiltering(resourceID);
- throw e;
- }
- }
-
@SuppressWarnings("unused")
- public R create(R target, P primary, Context
context) {
+ public R create(R desired, P primary, Context
context) {
if (useSSA(context)) {
// setting resource version for SSA so only created if it doesn't exist already
- target.getMetadata().setResourceVersion("1");
+ var createIfNotExisting = kubernetesDependentResourceConfig == null
+ ? KubernetesDependentResourceConfig.DEFAULT_CREATE_RESOURCE_ONLY_IF_NOT_EXISTING_WITH_SSA
+ : kubernetesDependentResourceConfig.createResourceOnlyIfNotExistingWithSSA();
+ if (createIfNotExisting) {
+ desired.getMetadata().setResourceVersion("1");
+ }
}
- final var resource = prepare(target, primary, "Creating");
+ addMetadata(false, null, desired, primary);
+ sanitizeDesired(desired, null, primary, context);
+ final var resource = prepare(desired, primary, "Creating");
return useSSA(context)
? resource
.fieldManager(context.getControllerConfiguration().fieldManager())
@@ -141,18 +124,20 @@ public R create(R target, P primary, Context
context) {
: resource.create();
}
- public R update(R actual, R target, P primary, Context
context) {
+ public R update(R actual, R desired, P primary, Context
context) {
if (log.isDebugEnabled()) {
log.debug("Updating actual resource: {} version: {}", ResourceID.fromResource(actual),
actual.getMetadata().getResourceVersion());
}
R updatedResource;
+ addMetadata(false, actual, desired, primary);
+ sanitizeDesired(desired, actual, primary, context);
if (useSSA(context)) {
- updatedResource = prepare(target, primary, "Updating")
+ updatedResource = prepare(desired, primary, "Updating")
.fieldManager(context.getControllerConfiguration().fieldManager())
.forceConflicts().serverSideApply();
} else {
- var updatedActual = updaterMatcher.updateResource(actual, target, context);
+ var updatedActual = updaterMatcher.updateResource(actual, desired, context);
updatedResource = prepare(updatedActual, primary, "Updating").update();
}
log.debug("Resource version after update: {}",
@@ -160,38 +145,70 @@ public R update(R actual, R target, P primary, Context
context) {
return updatedResource;
}
+ @Override
public Result match(R actualResource, P primary, Context context) {
final var desired = desired(primary, context);
+ sanitizeDesired(desired, actualResource, primary, context);
+ return match(actualResource, desired, primary, updaterMatcher, context);
+ }
+
+ @SuppressWarnings({"unused", "unchecked"})
+ public Result match(R actualResource, R desired, P primary, Context context) {
+ return match(actualResource, desired, primary,
+ (ResourceUpdaterMatcher) GenericResourceUpdaterMatcher
+ .updaterMatcherFor(actualResource.getClass()),
+ context);
+ }
+
+ public Result match(R actualResource, R desired, P primary, ResourceUpdaterMatcher matcher,
+ Context context) {
final boolean matches;
+ addMetadata(true, actualResource, desired, primary);
if (useSSA(context)) {
- addReferenceHandlingMetadata(desired, primary);
matches = SSABasedGenericKubernetesResourceMatcher.getInstance()
.matches(actualResource, desired, context);
} else {
- matches = updaterMatcher.matches(actualResource, desired, context);
+ matches = matcher.matches(actualResource, desired, context);
}
return Result.computed(matches, desired);
}
- @SuppressWarnings("unused")
- public Result match(R actualResource, R desired, P primary, Context context) {
- if (useSSA(context)) {
- addReferenceHandlingMetadata(desired, primary);
- var matches = SSABasedGenericKubernetesResourceMatcher.getInstance()
- .matches(actualResource, desired, context);
- return Result.computed(matches, desired);
- } else {
- return GenericKubernetesResourceMatcher
- .match(desired, actualResource, true,
- false, false, context);
+ protected void addMetadata(boolean forMatch, R actualResource, final R target, P primary) {
+ if (forMatch) { // keep the current
+ String actual = actualResource.getMetadata().getAnnotations()
+ .get(InformerEventSource.PREVIOUS_ANNOTATION_KEY);
+ Map annotations = target.getMetadata().getAnnotations();
+ if (actual != null) {
+ annotations.put(InformerEventSource.PREVIOUS_ANNOTATION_KEY, actual);
+ } else {
+ annotations.remove(InformerEventSource.PREVIOUS_ANNOTATION_KEY);
+ }
+ } else { // set a new one
+ eventSource().orElseThrow().addPreviousAnnotation(
+ Optional.ofNullable(actualResource).map(r -> r.getMetadata().getResourceVersion())
+ .orElse(null),
+ target);
}
+ addReferenceHandlingMetadata(target, primary);
+ }
+
+ protected void sanitizeDesired(R desired, R actual, P primary, Context context) {
+ DesiredResourceSanitizer.sanitizeDesired(desired, actual, primary, context, useSSA(context));
}
- private boolean useSSA(Context
context) {
- return context.getControllerConfiguration().getConfigurationService()
- .ssaBasedCreateUpdateMatchForDependentResources();
+ protected boolean useSSA(Context
context) {
+ Optional useSSAConfig =
+ configuration().flatMap(KubernetesDependentResourceConfig::useSSA);
+ var configService = context.getControllerConfiguration().getConfigurationService();
+ // don't use SSA for certain resources by default, only if explicitly overriden
+ if (useSSAConfig.isEmpty() && configService.defaultNonSSAResource().contains(resourceType())) {
+ return false;
+ }
+ return useSSAConfig.orElse(context.getControllerConfiguration().getConfigurationService()
+ .ssaBasedCreateUpdateMatchForDependentResources());
}
+ @Override
protected void handleDelete(P primary, R secondary, Context context) {
if (secondary != null) {
client.resource(secondary).delete();
@@ -203,19 +220,14 @@ public void deleteTargetResource(P primary, R resource, String key, Context
c
client.resource(resource).delete();
}
+ @SuppressWarnings("unused")
protected Resource prepare(R desired, P primary, String actionName) {
log.debug("{} target resource with type: {}, with id: {}",
actionName,
desired.getClass(),
ResourceID.fromResource(desired));
- addReferenceHandlingMetadata(desired, primary);
-
- if (desired instanceof Namespaced) {
- return client.resource(desired).inNamespace(desired.getMetadata().getNamespace());
- } else {
- return client.resource(desired);
- }
+ return client.resource(desired);
}
protected void addReferenceHandlingMetadata(R desired, P primary) {
@@ -249,7 +261,7 @@ protected InformerEventSource createEventSource(EventSourceContext cont
"Using default configuration for {} KubernetesDependentResource, call configureWith to provide configuration",
resourceType().getSimpleName());
}
- return (InformerEventSource) eventSource().orElseThrow();
+ return eventSource().orElseThrow();
}
private boolean useDefaultAnnotationsToIdentifyPrimary() {
@@ -258,10 +270,6 @@ private boolean useDefaultAnnotationsToIdentifyPrimary() {
private void addDefaultSecondaryToPrimaryMapperAnnotations(R desired, P primary) {
var annotations = desired.getMetadata().getAnnotations();
- if (annotations == null) {
- annotations = new HashMap<>();
- desired.getMetadata().setAnnotations(annotations);
- }
annotations.put(Mappers.DEFAULT_ANNOTATION_FOR_NAME, primary.getMetadata().getName());
var primaryNamespaces = primary.getMetadata().getNamespace();
if (primaryNamespaces != null) {
@@ -289,16 +297,6 @@ protected R desired(P primary, Context context) {
return super.desired(primary, context);
}
- private void prepareEventFiltering(R desired, ResourceID resourceID) {
- ((InformerEventSource) eventSource().orElseThrow())
- .prepareForCreateOrUpdateEventFiltering(resourceID, desired);
- }
-
- private void cleanupAfterEventFiltering(ResourceID resourceID) {
- ((InformerEventSource) eventSource().orElseThrow())
- .cleanupOnCreateOrUpdateEventFiltering(resourceID);
- }
-
@Override
public Optional> configuration() {
return Optional.ofNullable(kubernetesDependentResourceConfig);
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java
index 4047b25a13..9b3838831d 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java
@@ -1,5 +1,6 @@
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
+import java.util.Optional;
import java.util.Set;
import io.javaoperatorsdk.operator.api.reconciler.Constants;
@@ -13,41 +14,58 @@
public class KubernetesDependentResourceConfig {
- private Set namespaces = Constants.SAME_AS_CONTROLLER_NAMESPACES_SET;
- private String labelSelector = NO_VALUE_SET;
- private boolean namespacesWereConfigured = false;
- private ResourceDiscriminator resourceDiscriminator;
-
- private OnAddFilter onAddFilter;
-
- private OnUpdateFilter onUpdateFilter;
-
- private OnDeleteFilter onDeleteFilter;
-
- private GenericFilter genericFilter;
-
- public KubernetesDependentResourceConfig() {}
+ public static final boolean DEFAULT_CREATE_RESOURCE_ONLY_IF_NOT_EXISTING_WITH_SSA = true;
+
+ private Set namespaces;
+ private String labelSelector;
+ private final boolean namespacesWereConfigured;
+ private final boolean createResourceOnlyIfNotExistingWithSSA;
+ private final ResourceDiscriminator resourceDiscriminator;
+ private final Boolean useSSA;
+
+ private final OnAddFilter onAddFilter;
+ private final OnUpdateFilter onUpdateFilter;
+ private final OnDeleteFilter onDeleteFilter;
+ private final GenericFilter genericFilter;
+
+ public KubernetesDependentResourceConfig() {
+ this(Constants.SAME_AS_CONTROLLER_NAMESPACES_SET, NO_VALUE_SET, true,
+ DEFAULT_CREATE_RESOURCE_ONLY_IF_NOT_EXISTING_WITH_SSA,
+ null, null, null,
+ null, null, null);
+ }
- public KubernetesDependentResourceConfig(Set namespaces, String labelSelector,
- boolean configuredNS, ResourceDiscriminator resourceDiscriminator,
+ public KubernetesDependentResourceConfig(Set namespaces,
+ String labelSelector,
+ boolean configuredNS,
+ boolean createResourceOnlyIfNotExistingWithSSA,
+ ResourceDiscriminator resourceDiscriminator,
+ Boolean useSSA,
OnAddFilter onAddFilter,
OnUpdateFilter onUpdateFilter,
OnDeleteFilter onDeleteFilter, GenericFilter genericFilter) {
this.namespaces = namespaces;
this.labelSelector = labelSelector;
this.namespacesWereConfigured = configuredNS;
+ this.createResourceOnlyIfNotExistingWithSSA = createResourceOnlyIfNotExistingWithSSA;
this.onAddFilter = onAddFilter;
this.onUpdateFilter = onUpdateFilter;
this.onDeleteFilter = onDeleteFilter;
this.genericFilter = genericFilter;
this.resourceDiscriminator = resourceDiscriminator;
+ this.useSSA = useSSA;
}
+ // use builder instead
+ @Deprecated(forRemoval = true)
public KubernetesDependentResourceConfig(Set namespaces, String labelSelector) {
- this(namespaces, labelSelector, true, null, null, null,
- null, null);
+ this(namespaces, labelSelector, true, DEFAULT_CREATE_RESOURCE_ONLY_IF_NOT_EXISTING_WITH_SSA,
+ null, null, null,
+ null, null, null);
}
+ // use builder instead
+ @Deprecated(forRemoval = true)
public KubernetesDependentResourceConfig setLabelSelector(String labelSelector) {
this.labelSelector = labelSelector;
return this;
@@ -70,6 +88,9 @@ public OnAddFilter onAddFilter() {
return onAddFilter;
}
+ public boolean createResourceOnlyIfNotExistingWithSSA() {
+ return createResourceOnlyIfNotExistingWithSSA;
+ }
public OnUpdateFilter onUpdateFilter() {
return onUpdateFilter;
@@ -94,4 +115,8 @@ protected void setNamespaces(Set namespaces) {
this.namespaces = namespaces;
}
}
+
+ public Optional useSSA() {
+ return Optional.ofNullable(useSSA);
+ }
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfigBuilder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfigBuilder.java
new file mode 100644
index 0000000000..a18d8b8a41
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfigBuilder.java
@@ -0,0 +1,86 @@
+package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
+
+import java.util.Set;
+
+import io.javaoperatorsdk.operator.api.reconciler.Constants;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter;
+import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter;
+import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter;
+import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter;
+
+public final class KubernetesDependentResourceConfigBuilder {
+
+ private Set namespaces = Constants.SAME_AS_CONTROLLER_NAMESPACES_SET;
+ private String labelSelector;
+ private boolean createResourceOnlyIfNotExistingWithSSA;
+ private ResourceDiscriminator resourceDiscriminator;
+ private Boolean useSSA;
+ private OnAddFilter onAddFilter;
+ private OnUpdateFilter onUpdateFilter;
+ private OnDeleteFilter onDeleteFilter;
+ private GenericFilter genericFilter;
+
+ public KubernetesDependentResourceConfigBuilder() {}
+
+ public static KubernetesDependentResourceConfigBuilder aKubernetesDependentResourceConfig() {
+ return new KubernetesDependentResourceConfigBuilder<>();
+ }
+
+ public KubernetesDependentResourceConfigBuilder withNamespaces(Set namespaces) {
+ this.namespaces = namespaces;
+ return this;
+ }
+
+ public KubernetesDependentResourceConfigBuilder withLabelSelector(String labelSelector) {
+ this.labelSelector = labelSelector;
+ return this;
+ }
+
+ public KubernetesDependentResourceConfigBuilder withCreateResourceOnlyIfNotExistingWithSSA(
+ boolean createResourceOnlyIfNotExistingWithSSA) {
+ this.createResourceOnlyIfNotExistingWithSSA = createResourceOnlyIfNotExistingWithSSA;
+ return this;
+ }
+
+ public KubernetesDependentResourceConfigBuilder withResourceDiscriminator(
+ ResourceDiscriminator resourceDiscriminator) {
+ this.resourceDiscriminator = resourceDiscriminator;
+ return this;
+ }
+
+ public KubernetesDependentResourceConfigBuilder withUseSSA(Boolean useSSA) {
+ this.useSSA = useSSA;
+ return this;
+ }
+
+ public KubernetesDependentResourceConfigBuilder withOnAddFilter(OnAddFilter onAddFilter) {
+ this.onAddFilter = onAddFilter;
+ return this;
+ }
+
+ public KubernetesDependentResourceConfigBuilder withOnUpdateFilter(
+ OnUpdateFilter onUpdateFilter) {
+ this.onUpdateFilter = onUpdateFilter;
+ return this;
+ }
+
+ public KubernetesDependentResourceConfigBuilder withOnDeleteFilter(
+ OnDeleteFilter onDeleteFilter) {
+ this.onDeleteFilter = onDeleteFilter;
+ return this;
+ }
+
+ public KubernetesDependentResourceConfigBuilder withGenericFilter(
+ GenericFilter genericFilter) {
+ this.genericFilter = genericFilter;
+ return this;
+ }
+
+ public KubernetesDependentResourceConfig build() {
+ return new KubernetesDependentResourceConfig<>(namespaces, labelSelector,
+ namespaces != Constants.SAME_AS_CONTROLLER_NAMESPACES_SET,
+ createResourceOnlyIfNotExistingWithSSA, resourceDiscriminator, useSSA, onAddFilter,
+ onUpdateFilter, onDeleteFilter, genericFilter);
+ }
+}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java
index f4718b45c3..37a5fa9dd2 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java
@@ -1,14 +1,7 @@
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.Map.Entry;
-import java.util.Optional;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
import java.util.stream.Collectors;
import org.slf4j.Logger;
@@ -159,8 +152,8 @@ private static void fillResultsAndTraverseFurther(Map result,
Object managedFieldValue) {
var emptyMapValue = new HashMap();
result.put(keyInActual, emptyMapValue);
- var actualMapValue = actualMap.get(keyInActual);
- log.debug("key: {} actual map value: {} managedFieldValue: {}", keyInActual,
+ var actualMapValue = actualMap.getOrDefault(keyInActual, Collections.emptyMap());
+ log.trace("key: {} actual map value: {} managedFieldValue: {}", keyInActual,
actualMapValue, managedFieldValue);
keepOnlyManagedFields(emptyMapValue, (Map) actualMapValue,
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ClusterRoleBindingResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ClusterRoleBindingResourceUpdaterMatcher.java
deleted file mode 100644
index f6f6d1ef54..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ClusterRoleBindingResourceUpdaterMatcher.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.javaoperatorsdk.operator.processing.dependent.kubernetes.updatermatcher;
-
-import java.util.Objects;
-
-import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-public class ClusterRoleBindingResourceUpdaterMatcher
- extends GenericResourceUpdaterMatcher {
-
- @Override
- protected void updateClonedActual(ClusterRoleBinding actual, ClusterRoleBinding desired) {
- actual.setRoleRef(desired.getRoleRef());
- actual.setSubjects(desired.getSubjects());
- }
-
- @Override
- public boolean matches(ClusterRoleBinding actual, ClusterRoleBinding desired,
- Context> context) {
- return Objects.equals(actual.getRoleRef(), desired.getRoleRef()) &&
- Objects.equals(actual.getSubjects(), desired.getSubjects());
- }
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ClusterRoleResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ClusterRoleResourceUpdaterMatcher.java
deleted file mode 100644
index da7997c040..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ClusterRoleResourceUpdaterMatcher.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package io.javaoperatorsdk.operator.processing.dependent.kubernetes.updatermatcher;
-
-import java.util.Objects;
-
-import io.fabric8.kubernetes.api.model.rbac.ClusterRole;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-public class ClusterRoleResourceUpdaterMatcher
- extends GenericResourceUpdaterMatcher {
-
- @Override
- protected void updateClonedActual(ClusterRole actual, ClusterRole desired) {
- actual.setAggregationRule(desired.getAggregationRule());
- actual.setRules(desired.getRules());
- }
-
- @Override
- public boolean matches(ClusterRole actual, ClusterRole desired, Context> context) {
- return Objects.equals(actual.getRules(), desired.getRules()) &&
- Objects.equals(actual.getAggregationRule(), desired.getAggregationRule());
- }
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ConfigMapResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ConfigMapResourceUpdaterMatcher.java
deleted file mode 100644
index 7f89d45ff5..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ConfigMapResourceUpdaterMatcher.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package io.javaoperatorsdk.operator.processing.dependent.kubernetes.updatermatcher;
-
-import java.util.Objects;
-
-import io.fabric8.kubernetes.api.model.ConfigMap;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-public class ConfigMapResourceUpdaterMatcher
- extends GenericResourceUpdaterMatcher {
-
- @Override
- protected void updateClonedActual(ConfigMap actual, ConfigMap desired) {
- actual.setData(desired.getData());
- actual.setBinaryData((desired.getBinaryData()));
- actual.setImmutable(desired.getImmutable());
- }
-
- @Override
- public boolean matches(ConfigMap actual, ConfigMap desired, Context> context) {
- return Objects.equals(actual.getImmutable(), desired.getImmutable()) &&
- Objects.equals(actual.getData(), desired.getData()) &&
- Objects.equals(actual.getBinaryData(), desired.getBinaryData());
- }
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/EndpointSliceResourceUpdateMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/EndpointSliceResourceUpdateMatcher.java
deleted file mode 100644
index dc83c86dbf..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/EndpointSliceResourceUpdateMatcher.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package io.javaoperatorsdk.operator.processing.dependent.kubernetes.updatermatcher;
-
-import java.util.Objects;
-
-import io.fabric8.kubernetes.api.model.discovery.v1.EndpointSlice;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-public class EndpointSliceResourceUpdateMatcher
- extends GenericResourceUpdaterMatcher {
-
- @Override
- protected void updateClonedActual(EndpointSlice actual, EndpointSlice desired) {
- actual.setEndpoints(desired.getEndpoints());
- actual.setAddressType(desired.getAddressType());
- actual.setPorts(desired.getPorts());
- }
-
- @Override
- public boolean matches(EndpointSlice actual, EndpointSlice desired, Context> context) {
- return Objects.equals(actual.getEndpoints(), desired.getEndpoints()) &&
- Objects.equals(actual.getAddressType(), desired.getAddressType()) &&
- Objects.equals(actual.getPorts(), desired.getPorts());
- }
-
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/EndpointsResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/EndpointsResourceUpdaterMatcher.java
deleted file mode 100644
index 3dcfabde28..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/EndpointsResourceUpdaterMatcher.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package io.javaoperatorsdk.operator.processing.dependent.kubernetes.updatermatcher;
-
-import java.util.Objects;
-
-import io.fabric8.kubernetes.api.model.Endpoints;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-public class EndpointsResourceUpdaterMatcher extends GenericResourceUpdaterMatcher {
-
- @Override
- protected void updateClonedActual(Endpoints actual, Endpoints desired) {
- actual.setSubsets(desired.getSubsets());
- }
-
- @Override
- public boolean matches(Endpoints actual, Endpoints desired, Context> context) {
- return Objects.equals(actual.getSubsets(), desired.getSubsets());
- }
-
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/GenericResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/GenericResourceUpdaterMatcher.java
index cd9fbe2b16..2a5bae03b9 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/GenericResourceUpdaterMatcher.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/GenericResourceUpdaterMatcher.java
@@ -2,13 +2,8 @@
import java.util.Map;
-import io.fabric8.kubernetes.api.model.*;
-import io.fabric8.kubernetes.api.model.discovery.v1.EndpointSlice;
-import io.fabric8.kubernetes.api.model.rbac.ClusterRole;
-import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding;
-import io.fabric8.kubernetes.api.model.rbac.Role;
-import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
-import io.javaoperatorsdk.operator.ReconcilerUtils;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesResourceMatcher;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.ResourceUpdaterMatcher;
@@ -16,34 +11,31 @@
public class GenericResourceUpdaterMatcher implements
ResourceUpdaterMatcher {
+ private static final String METADATA = "metadata";
private static final ResourceUpdaterMatcher> INSTANCE = new GenericResourceUpdaterMatcher<>();
- @SuppressWarnings("rawtypes")
- private static final Map processors = Map.of(
- Secret.class, new SecretResourceUpdaterMatcher(),
- ConfigMap.class, new ConfigMapResourceUpdaterMatcher(),
- ServiceAccount.class, new ServiceAccountResourceUpdaterMatcher(),
- Role.class, new RoleResourceUpdaterMatcher(),
- ClusterRole.class, new ClusterRoleResourceUpdaterMatcher(),
- RoleBinding.class, new RoleBindingResourceUpdaterMatcher(),
- ClusterRoleBinding.class, new ClusterRoleBindingResourceUpdaterMatcher(),
- Endpoints.class, new EndpointsResourceUpdaterMatcher(),
- EndpointSlice.class, new EndpointSliceResourceUpdateMatcher());
-
protected GenericResourceUpdaterMatcher() {}
@SuppressWarnings("unchecked")
public static ResourceUpdaterMatcher updaterMatcherFor(
Class resourceType) {
- final var processor = processors.get(resourceType);
- return processor != null ? processor : (ResourceUpdaterMatcher) INSTANCE;
+ return (ResourceUpdaterMatcher) INSTANCE;
}
+ @SuppressWarnings("unchecked")
+ @Override
public R updateResource(R actual, R desired, Context> context) {
- var clonedActual = context.getControllerConfiguration().getConfigurationService()
- .getResourceCloner().clone(actual);
+ KubernetesSerialization kubernetesSerialization =
+ context.getClient().getKubernetesSerialization();
+ Map actualMap = kubernetesSerialization.convertValue(actual, Map.class);
+ Map desiredMap = kubernetesSerialization.convertValue(desired, Map.class);
+ // replace all top level fields from actual with desired, but merge metadata separately
+ var metadata = actualMap.remove(METADATA);
+ actualMap.replaceAll((k, v) -> desiredMap.get(k));
+ actualMap.putAll(desiredMap);
+ actualMap.put(METADATA, metadata);
+ var clonedActual = (R) kubernetesSerialization.convertValue(actualMap, desired.getClass());
updateLabelsAndAnnotation(clonedActual, desired);
- updateClonedActual(clonedActual, desired);
return clonedActual;
}
@@ -53,15 +45,6 @@ public boolean matches(R actual, R desired, Context> context) {
false, false, context).matched();
}
- protected void updateClonedActual(R actual, R desired) {
- updateSpec(actual, desired);
- }
-
- public static void updateSpec(K actual, K desired) {
- var desiredSpec = ReconcilerUtils.getSpec(desired);
- ReconcilerUtils.setSpec(actual, desiredSpec);
- }
-
public static void updateLabelsAndAnnotation(K actual, K desired) {
actual.getMetadata().getLabels().putAll(desired.getMetadata().getLabels());
actual.getMetadata().getAnnotations().putAll(desired.getMetadata().getAnnotations());
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/RoleBindingResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/RoleBindingResourceUpdaterMatcher.java
deleted file mode 100644
index c6a87cdae3..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/RoleBindingResourceUpdaterMatcher.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.javaoperatorsdk.operator.processing.dependent.kubernetes.updatermatcher;
-
-import java.util.Objects;
-
-import io.fabric8.kubernetes.api.model.rbac.RoleBinding;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-public class RoleBindingResourceUpdaterMatcher
- extends GenericResourceUpdaterMatcher {
-
- @Override
- protected void updateClonedActual(RoleBinding actual, RoleBinding desired) {
- actual.setRoleRef(desired.getRoleRef());
- actual.setSubjects(desired.getSubjects());
- }
-
- @Override
- public boolean matches(RoleBinding actual, RoleBinding desired,
- Context> context) {
- return Objects.equals(actual.getRoleRef(), desired.getRoleRef()) &&
- Objects.equals(actual.getSubjects(), desired.getSubjects());
- }
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/RoleResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/RoleResourceUpdaterMatcher.java
deleted file mode 100644
index f02d946db8..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/RoleResourceUpdaterMatcher.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package io.javaoperatorsdk.operator.processing.dependent.kubernetes.updatermatcher;
-
-import java.util.Objects;
-
-import io.fabric8.kubernetes.api.model.rbac.Role;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-public class RoleResourceUpdaterMatcher extends GenericResourceUpdaterMatcher {
-
- @Override
- protected void updateClonedActual(Role actual, Role desired) {
- actual.setRules(desired.getRules());
- }
-
- @Override
- public boolean matches(Role actual, Role desired, Context> context) {
- return Objects.equals(actual.getRules(), desired.getRules());
- }
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/SecretResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/SecretResourceUpdaterMatcher.java
deleted file mode 100644
index 14e8696704..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/SecretResourceUpdaterMatcher.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package io.javaoperatorsdk.operator.processing.dependent.kubernetes.updatermatcher;
-
-import java.util.Objects;
-
-import io.fabric8.kubernetes.api.model.Secret;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-public class SecretResourceUpdaterMatcher extends GenericResourceUpdaterMatcher {
-
- @Override
- protected void updateClonedActual(Secret actual, Secret desired) {
- actual.setData(desired.getData());
- actual.setStringData(desired.getStringData());
- actual.setImmutable(desired.getImmutable());
- actual.setType(desired.getType());
- }
-
- @Override
- public boolean matches(Secret actual, Secret desired, Context> context) {
- return Objects.equals(actual.getImmutable(), desired.getImmutable()) &&
- Objects.equals(actual.getType(), desired.getType()) &&
- Objects.equals(actual.getData(), desired.getData()) &&
- Objects.equals(actual.getStringData(), desired.getStringData());
- }
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ServiceAccountResourceUpdaterMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ServiceAccountResourceUpdaterMatcher.java
deleted file mode 100644
index f3d625c778..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/updatermatcher/ServiceAccountResourceUpdaterMatcher.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package io.javaoperatorsdk.operator.processing.dependent.kubernetes.updatermatcher;
-
-import java.util.Objects;
-
-import io.fabric8.kubernetes.api.model.ServiceAccount;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-
-public class ServiceAccountResourceUpdaterMatcher
- extends GenericResourceUpdaterMatcher {
-
- @Override
- protected void updateClonedActual(ServiceAccount actual, ServiceAccount desired) {
- actual.setAutomountServiceAccountToken(desired.getAutomountServiceAccountToken());
- actual.setImagePullSecrets(desired.getImagePullSecrets());
- actual.setSecrets(desired.getSecrets());
- }
-
- @Override
- public boolean matches(ServiceAccount actual, ServiceAccount desired, Context> context) {
- return Objects.equals(actual.getAutomountServiceAccountToken(),
- desired.getAutomountServiceAccountToken()) &&
- Objects.equals(actual.getImagePullSecrets(), desired.getImagePullSecrets()) &&
- Objects.equals(actual.getSecrets(), desired.getSecrets());
- }
-}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java
index e5b89f6c80..13d51b1759 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java
@@ -81,6 +81,7 @@ public Workflow resolve(KubernetesClient client,
spec.getReconcileCondition(),
spec.getDeletePostCondition(),
spec.getReadyCondition(),
+ spec.getActivationCondition(),
resolve(spec, client, configuration));
alreadyResolved.put(node.getName(), node);
spec.getDependsOn()
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java
index 1b12970f48..476d87765e 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java
@@ -16,19 +16,21 @@ public class DependentResourceNode {
private Condition reconcilePrecondition;
private Condition deletePostcondition;
private Condition readyPostcondition;
+ private Condition activationCondition;
private final DependentResource dependentResource;
DependentResourceNode(DependentResource dependentResource) {
- this(getNameFor(dependentResource), null, null, null, dependentResource);
+ this(getNameFor(dependentResource), null, null, null, null, dependentResource);
}
public DependentResourceNode(String name, Condition reconcilePrecondition,
Condition deletePostcondition, Condition readyPostcondition,
- DependentResource dependentResource) {
+ Condition activationCondition, DependentResource dependentResource) {
this.name = name;
this.reconcilePrecondition = reconcilePrecondition;
this.deletePostcondition = deletePostcondition;
this.readyPostcondition = readyPostcondition;
+ this.activationCondition = activationCondition;
this.dependentResource = dependentResource;
}
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 78f42e3bed..9a110a7ba8 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
@@ -201,13 +201,17 @@ public boolean isLastAttempt() {
.updateStatus(errorStatusUpdateControl.getResource().orElseThrow());
}
if (errorStatusUpdateControl.isNoRetry()) {
+ PostExecutionControl postExecutionControl;
if (updatedResource != null) {
- return errorStatusUpdateControl.isPatch()
+ postExecutionControl = errorStatusUpdateControl.isPatch()
? PostExecutionControl.customResourceStatusPatched(updatedResource)
: PostExecutionControl.customResourceUpdated(updatedResource);
} else {
- return PostExecutionControl.defaultDispatch();
+ postExecutionControl = PostExecutionControl.defaultDispatch();
}
+ errorStatusUpdateControl.getScheduleDelay()
+ .ifPresent(postExecutionControl::withReSchedule);
+ return postExecutionControl;
}
} catch (RuntimeException ex) {
log.error("Error during error status handling.", ex);
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/IndexerResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/IndexerResourceCache.java
index e52833d00a..3938d8219b 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/IndexerResourceCache.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/IndexerResourceCache.java
@@ -5,15 +5,13 @@
import java.util.function.Function;
import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.javaoperatorsdk.operator.api.reconciler.IndexedResourceCache;
-public interface IndexerResourceCache extends ResourceCache {
+public interface IndexerResourceCache extends IndexedResourceCache {
void addIndexers(Map>> indexers);
default void addIndexer(String name, Function> indexer) {
addIndexers(Map.of(name, indexer));
}
-
- List byIndex(String indexName, String indexKey);
-
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java
deleted file mode 100644
index 5d23d870aa..0000000000
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package io.javaoperatorsdk.operator.processing.event.source.informer;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import io.fabric8.kubernetes.api.model.HasMetadata;
-import io.javaoperatorsdk.operator.processing.event.ResourceID;
-
-public class EventRecorder {
-
- private final Map> resourceEvents = new HashMap<>();
-
- public void startEventRecording(ResourceID resourceID) {
- resourceEvents.putIfAbsent(resourceID, new ArrayList<>(5));
- }
-
- public boolean isRecordingFor(ResourceID resourceID) {
- return resourceEvents.get(resourceID) != null;
- }
-
- public void stopEventRecording(ResourceID resourceID) {
- resourceEvents.remove(resourceID);
- }
-
- public void recordEvent(R resource) {
- resourceEvents.get(ResourceID.fromResource(resource)).add(resource);
- }
-
- public boolean containsEventWithResourceVersion(ResourceID resourceID,
- String resourceVersion) {
- List events = resourceEvents.get(resourceID);
- if (events == null) {
- return false;
- }
- if (events.isEmpty()) {
- return false;
- } else {
- return events.stream()
- .anyMatch(e -> e.getMetadata().getResourceVersion().equals(resourceVersion));
- }
- }
-
- public boolean containsEventWithVersionButItsNotLastOne(
- ResourceID resourceID, String resourceVersion) {
- List resources = resourceEvents.get(resourceID);
- if (resources == null) {
- throw new IllegalStateException(
- "Null events list, this is probably a result of invalid usage of the " +
- "InformerEventSource. Resource ID: " + resourceID);
- }
- if (resources.isEmpty()) {
- throw new IllegalStateException("No events for resource id: " + resourceID);
- }
- return !resources
- .get(resources.size() - 1)
- .getMetadata()
- .getResourceVersion()
- .equals(resourceVersion);
- }
-
- public R getLastEvent(ResourceID resourceID) {
- List resources = resourceEvents.get(resourceID);
- if (resources == null) {
- throw new IllegalStateException(
- "Null events list, this is probably a result of invalid usage of the " +
- "InformerEventSource. Resource ID: " + resourceID);
- }
- return resources.get(resources.size() - 1);
- }
-}
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 a9a07f2332..d154ad201b 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
@@ -14,7 +14,6 @@
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
-import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationEventFilter;
import io.javaoperatorsdk.operator.processing.event.Event;
import io.javaoperatorsdk.operator.processing.event.EventHandler;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
@@ -68,16 +67,17 @@
*/
public class InformerEventSource
extends ManagedInformerEventSource>
- implements ResourceEventHandler, RecentOperationEventFilter {
+ implements ResourceEventHandler {
+
+ public static String PREVIOUS_ANNOTATION_KEY = "javaoperatorsdk.io/previous";
private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class);
- // always called from a synchronized method
- private final EventRecorder eventRecorder = new EventRecorder<>();
// 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 Map>> indexerBuffer = new HashMap<>();
+ private final String id = UUID.randomUUID().toString();
public InformerEventSource(
InformerConfiguration configuration, EventSourceContext context) {
@@ -147,12 +147,8 @@ public void onDelete(R resource, boolean b) {
private synchronized void onAddOrUpdate(Operation operation, R newObject, R oldObject,
Runnable superOnOp) {
var resourceID = ResourceID.fromResource(newObject);
- if (eventRecorder.isRecordingFor(resourceID)) {
- log.debug("Recording event for: {}", resourceID);
- eventRecorder.recordEvent(newObject);
- return;
- }
- if (temporaryCacheHasResourceWithSameVersionAs(newObject)) {
+
+ if (canSkipEvent(newObject, oldObject, resourceID)) {
log.debug(
"Skipping event propagation for {}, since was a result of a reconcile action. Resource ID: {}",
operation,
@@ -172,16 +168,33 @@ private synchronized void onAddOrUpdate(Operation operation, R newObject, R oldO
}
}
- private boolean temporaryCacheHasResourceWithSameVersionAs(R resource) {
- var resourceID = ResourceID.fromResource(resource);
+ private boolean canSkipEvent(R newObject, R oldObject, ResourceID resourceID) {
var res = temporaryResourceCache.getResourceFromCache(resourceID);
- return res.map(r -> {
- boolean resVersionsEqual = r.getMetadata().getResourceVersion()
- .equals(resource.getMetadata().getResourceVersion());
- log.debug("Resource found in temporal cache for id: {} resource versions equal: {}",
- resourceID, resVersionsEqual);
- return resVersionsEqual;
- }).orElse(false);
+ 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;
+ }
+
+ 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) {
@@ -239,99 +252,24 @@ public InformerConfiguration getConfiguration() {
@Override
public synchronized void handleRecentResourceUpdate(ResourceID resourceID, R resource,
R previousVersionOfResource) {
- handleRecentCreateOrUpdate(Operation.UPDATE, resource, previousVersionOfResource,
- () -> super.handleRecentResourceUpdate(resourceID, resource, previousVersionOfResource));
+ handleRecentCreateOrUpdate(Operation.UPDATE, resource, previousVersionOfResource);
}
@Override
public synchronized void handleRecentResourceCreate(ResourceID resourceID, R resource) {
- handleRecentCreateOrUpdate(Operation.ADD, resource, null,
- () -> super.handleRecentResourceCreate(resourceID, resource));
+ handleRecentCreateOrUpdate(Operation.ADD, resource, null);
}
- private void handleRecentCreateOrUpdate(Operation operation, R resource, R oldResource,
- Runnable runnable) {
- primaryToSecondaryIndex.onAddOrUpdate(resource);
- if (eventRecorder.isRecordingFor(ResourceID.fromResource(resource))) {
- handleRecentResourceOperationAndStopEventRecording(operation, resource, oldResource);
- } else {
- runnable.run();
- }
- }
-
- /**
- * There can be the following cases:
- *
- * - 1. Did not receive the event yet for the target resource, then we need to put it to temp
- * cache. Because event will arrive. Note that this not necessary mean that the even is not sent
- * yet (we are in sync context). Also does not mean that there are no more events received after
- * that. But during the event processing (onAdd, onUpdate) we make sure that the propagation just
- * skipped for the right event.
- * - 2. Received the event about the operation already, it was the last. This means already is
- * on cache of informer. So we have to do nothing. Since it was just recorded and not propagated.
- *
- * - 3. Received the event but more events received since, so those were not propagated yet. So
- * an event needs to be propagated to compensate.
- *
- *
- * @param newResource just created or updated resource
- */
- private void handleRecentResourceOperationAndStopEventRecording(Operation operation,
- R newResource, R oldResource) {
- ResourceID resourceID = ResourceID.fromResource(newResource);
- try {
- if (!eventRecorder.containsEventWithResourceVersion(
- resourceID, newResource.getMetadata().getResourceVersion())) {
- log.debug(
- "Did not found event in buffer with target version and resource id: {}", resourceID);
- temporaryResourceCache.unconditionallyCacheResource(newResource);
- } else {
- // if the resource is not added to the temp cache, it is cleared, since
- // the cache is cleared by subsequent events after updates, but if those did not receive
- // the temp cache is still filled at this point with an old resource
- log.debug("Cleaning temporary cache for resource id: {}", resourceID);
- temporaryResourceCache.removeResourceFromCache(newResource);
- if (eventRecorder.containsEventWithVersionButItsNotLastOne(
- resourceID, newResource.getMetadata().getResourceVersion())) {
- R lastEvent = eventRecorder.getLastEvent(resourceID);
-
- log.debug(
- "Found events in event buffer but the target event is not last for id: {}. Propagating event.",
- resourceID);
- if (eventAcceptedByFilter(operation, newResource, oldResource)) {
- propagateEvent(lastEvent);
- }
- }
- }
- } finally {
- log.debug("Stopping event recording for: {}", resourceID);
- eventRecorder.stopEventRecording(resourceID);
- }
+ private void handleRecentCreateOrUpdate(Operation operation, R newResource, R oldResource) {
+ primaryToSecondaryIndex.onAddOrUpdate(newResource);
+ temporaryResourceCache.putResource(newResource, Optional.ofNullable(oldResource)
+ .map(r -> r.getMetadata().getResourceVersion()).orElse(null));
}
private boolean useSecondaryToPrimaryIndex() {
return this.primaryToSecondaryMapper == null;
}
- @Override
- public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID,
- R resource) {
- log.debug("Starting event recording for: {}", resourceID);
- eventRecorder.startEventRecording(resourceID);
- }
-
- /**
- * Mean to be called to clean up in case of an exception from the client. Usually in a catch
- * block.
- *
- * @param resourceID to cleanup
- */
- @Override
- public synchronized void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID) {
- log.debug("Stopping event recording for: {}", resourceID);
- eventRecorder.stopEventRecording(resourceID);
- }
-
@Override
public boolean allowsNamespaceChanges() {
return configuration().followControllerNamespaceChanges();
@@ -361,6 +299,7 @@ private boolean acceptedByDeleteFilters(R resource, boolean b) {
// Since this event source instance is created by the user, the ConfigurationService is actually
// injected after it is registered. Some of the subcomponents are initialized at that time here.
+ @Override
public void setConfigurationService(ConfigurationService configurationService) {
super.setConfigurationService(configurationService);
@@ -368,6 +307,7 @@ public void setConfigurationService(ConfigurationService configurationService) {
indexerBuffer = new HashMap<>();
}
+ @Override
public void addIndexers(Map>> indexers) {
if (indexerBuffer == null) {
throw new OperatorException("Cannot add indexers after InformerEventSource started.");
@@ -375,4 +315,16 @@ public void addIndexers(Map>> indexers) {
indexerBuffer.putAll(indexers);
}
+ /**
+ * 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;
+ }
+
}
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 02df5c2ee5..6ec6cd7f6e 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
@@ -90,7 +90,7 @@ public void stop() {
@Override
public void handleRecentResourceUpdate(ResourceID resourceID, R resource,
R previousVersionOfResource) {
- temporaryResourceCache.putUpdatedResource(resource,
+ temporaryResourceCache.putResource(resource,
previousVersionOfResource.getMetadata().getResourceVersion());
}
@@ -131,6 +131,7 @@ public void addIndexers(Map>> indexers) {
cache.addIndexers(indexers);
}
+ @Override
public List byIndex(String indexName, String indexKey) {
return manager().byIndex(indexName, indexKey);
}
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 8c3d56c008..233b409f3f 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
@@ -41,40 +41,37 @@ public TemporaryResourceCache(ManagedInformerEventSource managedInforme
this.managedInformerEventSource = managedInformerEventSource;
}
- public synchronized void removeResourceFromCache(T resource) {
- cache.remove(ResourceID.fromResource(resource));
- }
-
- public synchronized void unconditionallyCacheResource(T newResource) {
- putToCache(newResource, null);
+ public synchronized Optional removeResourceFromCache(T resource) {
+ return Optional.ofNullable(cache.remove(ResourceID.fromResource(resource)));
}
public synchronized void putAddedResource(T newResource) {
- ResourceID resourceID = ResourceID.fromResource(newResource);
- if (managedInformerEventSource.get(resourceID).isEmpty()) {
- log.debug("Putting resource to cache with ID: {}", resourceID);
- putToCache(newResource, resourceID);
- } else {
- log.debug("Won't put resource into cache found already informer cache: {}", resourceID);
- }
+ putResource(newResource, null);
}
- public synchronized void putUpdatedResource(T newResource, String previousResourceVersion) {
+ /**
+ * 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 informerCacheResource = managedInformerEventSource.get(resourceId);
- if (informerCacheResource.isEmpty()) {
- log.debug("No cached value present for resource: {}", newResource);
- return;
- }
- // if this is not true that means the cache was already updated
- if (informerCacheResource.get().getMetadata().getResourceVersion()
- .equals(previousResourceVersion)) {
- log.debug("Putting resource to temporal cache with id: {}", resourceId);
+ var cachedResource = getResourceFromCache(resourceId)
+ .orElse(managedInformerEventSource.get(resourceId).orElse(null));
+
+ if ((previousResourceVersion == null && cachedResource == null)
+ || (cachedResource != null && previousResourceVersion != null
+ && cachedResource.getMetadata().getResourceVersion()
+ .equals(previousResourceVersion))) {
+ log.debug(
+ "Temporarily moving ahead to target version {} for resource id: {}",
+ newResource.getMetadata().getResourceVersion(), resourceId);
putToCache(newResource, resourceId);
} else {
- // if something is in cache it's surely obsolete now
- log.debug("Trying to remove an obsolete resource from cache for id: {}", resourceId);
- cache.remove(resourceId);
+ if (cache.remove(resourceId) != null) {
+ log.debug("Removed an obsolete resource from cache for id: {}", resourceId);
+ }
}
}
diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java
index 71be6a2cd4..eae4c22ac6 100644
--- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java
+++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java
@@ -20,6 +20,7 @@
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfigBuilder;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -327,7 +328,10 @@ void replaceNamedDependentResourceConfigShouldWork() {
final var overridden = ControllerConfigurationOverrider.override(configuration)
.replacingNamedDependentResourceConfig(
DependentResource.defaultNameFor(ReadOnlyDependent.class),
- new KubernetesDependentResourceConfig(Set.of(overriddenNS), labelSelector))
+ new KubernetesDependentResourceConfigBuilder<>()
+ .withNamespaces(Set.of(overriddenNS))
+ .withLabelSelector(labelSelector)
+ .build())
.build();
dependents = overridden.getDependentResources();
dependentSpec = dependents.stream().filter(dr -> dr.getName().equals(dependentResourceName))
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 41bafe00e4..b1ff214f3b 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
@@ -10,6 +10,7 @@
import io.fabric8.kubernetes.api.model.ServiceAccountBuilder;
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.javaoperatorsdk.operator.MockKubernetesClient;
import io.javaoperatorsdk.operator.ReconcilerUtils;
import io.javaoperatorsdk.operator.api.reconciler.Context;
@@ -79,6 +80,15 @@ void doesNotMatchChangedValues() {
.isFalse();
}
+ @Test
+ void ignoreStatus() {
+ actual = createDeployment();
+ actual.setStatus(new DeploymentStatusBuilder().withReadyReplicas(1).build());
+ assertThat(matcher.match(actual, null, context).matched())
+ .withFailMessage("Should ignore status in actual")
+ .isTrue();
+ }
+
@Test
void doesNotMatchChangedValuesWhenNoIgnoredPathsAreProvided() {
actual = createDeployment();
diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterMatcherTest.java
index c1d9b4a5a5..44f3fb51ea 100644
--- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterMatcherTest.java
+++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdaterMatcherTest.java
@@ -2,6 +2,7 @@
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -33,7 +34,7 @@ static void setUp() {
final var client = MockKubernetesClient.client(HasMetadata.class);
when(configService.getKubernetesClient()).thenReturn(client);
when(configService.getResourceCloner()).thenCallRealMethod();
-
+ when(context.getClient()).thenReturn(client);
when(context.getControllerConfiguration()).thenReturn(controllerConfiguration);
}
@@ -99,6 +100,23 @@ void checkSecret() {
assertThat(secret.getData()).containsOnlyKeys("foo");
}
+ @Test
+ void checkSeviceAccount() {
+ var processor = GenericResourceUpdaterMatcher.updaterMatcherFor(ServiceAccount.class);
+ var desired = new ServiceAccountBuilder()
+ .withMetadata(new ObjectMetaBuilder().addToLabels("new", "label").build())
+ .build();
+ var actual = new ServiceAccountBuilder()
+ .withMetadata(new ObjectMetaBuilder().addToLabels("a", "label").build())
+ .withImagePullSecrets(new LocalObjectReferenceBuilder().withName("secret").build())
+ .build();
+
+ final var serviceAccount = processor.updateResource(actual, desired, context);
+ assertThat(serviceAccount.getMetadata().getLabels())
+ .isEqualTo(Map.of("a", "label", "new", "label"));
+ assertThat(serviceAccount.getImagePullSecrets()).isNullOrEmpty();
+ }
+
Deployment createDeployment() {
return ReconcilerUtils.loadYaml(
Deployment.class, GenericResourceUpdaterMatcherTest.class, "nginx-deployment.yaml");
diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java
index 25c0ad139b..b314b5b112 100644
--- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java
+++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java
@@ -21,7 +21,7 @@ public class ManagedWorkflowTestUtils {
@SuppressWarnings("unchecked")
public static DependentResourceSpec createDRS(String name, String... dependOns) {
return new DependentResourceSpec(EmptyTestDependentResource.class, name, Set.of(dependOns),
- null, null, null, null);
+ null, null, null, null, null);
}
public static DependentResourceSpec createDRSWithTraits(String name,
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 9b113625df..c6aacb9071 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
@@ -673,6 +673,24 @@ void retriesAddingFinalizer() {
verify(customResourceFacade, times(2)).updateResource(any());
}
+ @Test
+ void reSchedulesFromErrorHandler() {
+ var delay = 1000L;
+ testCustomResource.addFinalizer(DEFAULT_FINALIZER);
+ reconciler.reconcile = (r, c) -> {
+ throw new IllegalStateException("Error Status Test");
+ };
+ reconciler.errorHandler =
+ (r, ri, e) -> ErrorStatusUpdateControl.noStatusUpdate()
+ .rescheduleAfter(delay);
+
+ var res = reconciliationDispatcher.handleExecution(
+ new ExecutionScope(null).setResource(testCustomResource));
+
+ assertThat(res.getReScheduleDelay()).contains(delay);
+ assertThat(res.getRuntimeException()).isEmpty();
+ }
+
private ObservedGenCustomResource createObservedGenCustomResource() {
ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource();
observedGenCustomResource.setMetadata(new ObjectMeta());
diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorderTest.java
deleted file mode 100644
index 556ad089ff..0000000000
--- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorderTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package io.javaoperatorsdk.operator.processing.event.source.informer;
-
-import org.junit.jupiter.api.Test;
-
-import io.fabric8.kubernetes.api.model.ConfigMap;
-import io.fabric8.kubernetes.api.model.ObjectMeta;
-import io.javaoperatorsdk.operator.processing.event.ResourceID;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-class EventRecorderTest {
-
- public static final String RESOURCE_VERSION = "0";
- public static final String RESOURCE_VERSION1 = "1";
-
- EventRecorder eventRecorder = new EventRecorder<>();
-
- ConfigMap testConfigMap = testConfigMap(RESOURCE_VERSION);
- ConfigMap testConfigMap2 = testConfigMap(RESOURCE_VERSION1);
-
- ResourceID id = ResourceID.fromResource(testConfigMap);
-
- @Test
- void recordsEvents() {
-
- assertThat(eventRecorder.isRecordingFor(id)).isFalse();
-
- eventRecorder.startEventRecording(id);
- assertThat(eventRecorder.isRecordingFor(id)).isTrue();
-
- eventRecorder.recordEvent(testConfigMap);
-
- eventRecorder.stopEventRecording(id);
- assertThat(eventRecorder.isRecordingFor(id)).isFalse();
- }
-
- @Test
- void getsLastRecorded() {
- eventRecorder.startEventRecording(id);
-
- eventRecorder.recordEvent(testConfigMap);
- eventRecorder.recordEvent(testConfigMap2);
-
- assertThat(eventRecorder.getLastEvent(id)).isEqualTo(testConfigMap2);
- }
-
- @Test
- void checksContainsWithResourceVersion() {
- eventRecorder.startEventRecording(id);
-
- eventRecorder.recordEvent(testConfigMap);
- eventRecorder.recordEvent(testConfigMap2);
-
- assertThat(eventRecorder.containsEventWithResourceVersion(id, RESOURCE_VERSION)).isTrue();
- assertThat(eventRecorder.containsEventWithResourceVersion(id, RESOURCE_VERSION1)).isTrue();
- assertThat(eventRecorder.containsEventWithResourceVersion(id, "xxx")).isFalse();
- }
-
- @Test
- void checkLastItemVersion() {
- eventRecorder.startEventRecording(id);
-
- eventRecorder.recordEvent(testConfigMap);
- eventRecorder.recordEvent(testConfigMap2);
-
- assertThat(eventRecorder.containsEventWithVersionButItsNotLastOne(id, RESOURCE_VERSION))
- .isTrue();
- assertThat(eventRecorder.containsEventWithVersionButItsNotLastOne(id, RESOURCE_VERSION1))
- .isFalse();
- }
-
- ConfigMap testConfigMap(String resourceVersion) {
- ConfigMap configMap = new ConfigMap();
- configMap.setMetadata(new ObjectMeta());
- configMap.getMetadata().setName("test");
- configMap.getMetadata().setResourceVersion(resourceVersion);
-
- return configMap;
- }
-
-}
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 69e26f0b35..7acecc7099 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
@@ -8,6 +8,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.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.MockKubernetesClient;
import io.javaoperatorsdk.operator.OperatorException;
@@ -36,7 +37,6 @@ class InformerEventSourceTest {
private static final String PREV_RESOURCE_VERSION = "0";
private static final String DEFAULT_RESOURCE_VERSION = "1";
- private static final String NEXT_RESOURCE_VERSION = "2";
private InformerEventSource informerEventSource;
private final KubernetesClient clientMock = MockKubernetesClient.client(Deployment.class);
@@ -78,126 +78,48 @@ void skipsEventPropagationIfResourceWithSameVersionInResourceCache() {
}
@Test
- void propagateEventAndRemoveResourceFromTempCacheIfResourceVersionMismatch() {
- Deployment cachedDeployment = testDeployment();
- cachedDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION);
- when(temporaryResourceCacheMock.getResourceFromCache(any()))
- .thenReturn(Optional.of(cachedDeployment));
-
-
- informerEventSource.onUpdate(cachedDeployment, testDeployment());
+ void skipsAddEventPropagationViaAnnotation() {
+ informerEventSource.onAdd(informerEventSource.addPreviousAnnotation(null, testDeployment()));
- verify(eventHandlerMock, times(1)).handleEvent(any());
- verify(temporaryResourceCacheMock, times(1)).removeResourceFromCache(any());
+ verify(eventHandlerMock, never()).handleEvent(any());
}
@Test
- void notPropagatesEventIfAfterUpdateReceivedJustTheRelatedEvent() {
- var testDeployment = testDeployment();
- var prevTestDeployment = testDeployment();
- prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION);
-
+ void skipsUpdateEventPropagationViaAnnotation() {
+ informerEventSource.onUpdate(testDeployment(),
+ informerEventSource.addPreviousAnnotation("1", testDeployment()));
- informerEventSource
- .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment),
- testDeployment);
- informerEventSource.onUpdate(prevTestDeployment, testDeployment);
- informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment),
- testDeployment, prevTestDeployment);
-
- verify(eventHandlerMock, times(0)).handleEvent(any());
- verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any());
+ verify(eventHandlerMock, never()).handleEvent(any());
}
-
@Test
- void notPropagatesEventIfAfterCreateReceivedJustTheRelatedEvent() {
- var testDeployment = testDeployment();
-
- informerEventSource
- .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment),
- testDeployment);
- informerEventSource.onAdd(testDeployment);
- informerEventSource.handleRecentResourceCreate(ResourceID.fromResource(testDeployment),
- testDeployment);
-
- verify(eventHandlerMock, times(0)).handleEvent(any());
- verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any());
+ void processEventPropagationWithoutAnnotation() {
+ informerEventSource.onUpdate(testDeployment(), testDeployment());
+
+ verify(eventHandlerMock, times(1)).handleEvent(any());
}
@Test
- void propagatesEventIfNewEventReceivedAfterTheCurrentTargetEvent() {
- var testDeployment = testDeployment();
- var prevTestDeployment = testDeployment();
- prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION);
- var nextTestDeployment = testDeployment();
- nextTestDeployment.getMetadata().setResourceVersion(NEXT_RESOURCE_VERSION);
-
- informerEventSource
- .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment),
- testDeployment);
- informerEventSource.onUpdate(prevTestDeployment, testDeployment);
- informerEventSource.onUpdate(testDeployment, nextTestDeployment);
- informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment),
- testDeployment, prevTestDeployment);
+ void processEventPropagationWithIncorrectAnnotation() {
+ informerEventSource.onAdd(new DeploymentBuilder(testDeployment()).editMetadata()
+ .addToAnnotations(InformerEventSource.PREVIOUS_ANNOTATION_KEY, "invalid")
+ .endMetadata().build());
verify(eventHandlerMock, times(1)).handleEvent(any());
- verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any());
}
@Test
- void notPropagatesEventIfMoreReceivedButTheLastIsTheUpdated() {
- var testDeployment = testDeployment();
- var prevTestDeployment = testDeployment();
- prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION);
- var prevPrevTestDeployment = testDeployment();
- prevPrevTestDeployment.getMetadata().setResourceVersion("-1");
-
- informerEventSource
- .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment),
- testDeployment);
- informerEventSource.onUpdate(prevPrevTestDeployment, prevTestDeployment);
- informerEventSource.onUpdate(prevTestDeployment, testDeployment);
- informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment),
- testDeployment, prevTestDeployment);
-
- verify(eventHandlerMock, times(0)).handleEvent(any());
- verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any());
- }
+ void propagateEventAndRemoveResourceFromTempCacheIfResourceVersionMismatch() {
+ Deployment cachedDeployment = testDeployment();
+ cachedDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION);
+ when(temporaryResourceCacheMock.getResourceFromCache(any()))
+ .thenReturn(Optional.of(cachedDeployment));
- @Test
- void putsResourceOnTempCacheIfNoEventRecorded() {
- var testDeployment = testDeployment();
- var prevTestDeployment = testDeployment();
- prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION);
-
- informerEventSource
- .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment),
- testDeployment);
- informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment),
- testDeployment, prevTestDeployment);
-
- verify(eventHandlerMock, times(0)).handleEvent(any());
- verify(temporaryResourceCacheMock, times(1)).unconditionallyCacheResource(any());
- }
- @Test
- void putsResourceOnTempCacheIfNoEventRecordedWithSameResourceVersion() {
- var testDeployment = testDeployment();
- var prevTestDeployment = testDeployment();
- prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION);
- var prevPrevTestDeployment = testDeployment();
- prevPrevTestDeployment.getMetadata().setResourceVersion("-1");
-
- informerEventSource
- .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment),
- testDeployment);
- informerEventSource.onUpdate(prevPrevTestDeployment, prevTestDeployment);
- informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment),
- testDeployment, prevTestDeployment);
-
- verify(eventHandlerMock, times(0)).handleEvent(any());
- verify(temporaryResourceCacheMock, times(1)).unconditionallyCacheResource(any());
+ informerEventSource.onUpdate(cachedDeployment, testDeployment());
+
+ verify(eventHandlerMock, times(1)).handleEvent(any());
+ verify(temporaryResourceCacheMock, times(1)).removeResourceFromCache(any());
}
@Test
diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java
index f848e26cec..4d5bdf0dfd 100644
--- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java
+++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java
@@ -30,7 +30,7 @@ void updateAddsTheResourceIntoCacheIfTheInformerHasThePreviousResourceVersion()
prevTestResource.getMetadata().setResourceVersion("0");
when(informerEventSource.get(any())).thenReturn(Optional.of(prevTestResource));
- temporaryResourceCache.putUpdatedResource(testResource, "0");
+ temporaryResourceCache.putResource(testResource, "0");
var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource));
assertThat(cached).isPresent();
@@ -43,7 +43,7 @@ void updateNotAddsTheResourceIntoCacheIfTheInformerHasOtherVersion() {
informerCachedResource.getMetadata().setResourceVersion("x");
when(informerEventSource.get(any())).thenReturn(Optional.of(informerCachedResource));
- temporaryResourceCache.putUpdatedResource(testResource, "0");
+ temporaryResourceCache.putResource(testResource, "0");
var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource));
assertThat(cached).isNotPresent();
diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml
index 8e48eac880..de852a1dab 100644
--- a/operator-framework-junit5/pom.xml
+++ b/operator-framework-junit5/pom.xml
@@ -5,7 +5,7 @@
java-operator-sdk
io.javaoperatorsdk
- 4.4.4-SNAPSHOT
+ 4.5.0-SNAPSHOT
4.0.0
diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml
index 9b89e9a52d..2b74b5e91f 100644
--- a/operator-framework/pom.xml
+++ b/operator-framework/pom.xml
@@ -5,7 +5,7 @@
java-operator-sdk
io.javaoperatorsdk
- 4.4.4-SNAPSHOT
+ 4.5.0-SNAPSHOT
4.0.0
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateOnlyIfNotExistingDependentWithSSA.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateOnlyIfNotExistingDependentWithSSA.java
new file mode 100644
index 0000000000..8a347f632e
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateOnlyIfNotExistingDependentWithSSA.java
@@ -0,0 +1,57 @@
+package io.javaoperatorsdk.operator;
+
+import java.time.Duration;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
+import io.javaoperatorsdk.operator.sample.createonlyifnotexistsdependentwithssa.CreateOnlyIfNotExistingDependentWithSSACustomResource;
+import io.javaoperatorsdk.operator.sample.createonlyifnotexistsdependentwithssa.CreateOnlyIfNotExistingDependentWithSSAReconciler;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+class CreateOnlyIfNotExistingDependentWithSSA {
+
+ public static final String TEST_RESOURCE_NAME = "test1";
+ public static final String KEY = "key";
+
+ @RegisterExtension
+ LocallyRunOperatorExtension extension =
+ LocallyRunOperatorExtension.builder()
+ .withReconciler(new CreateOnlyIfNotExistingDependentWithSSAReconciler())
+ .build();
+
+
+ @Test
+ void createsResourceOnlyIfNotExisting() {
+ var cm = new ConfigMapBuilder().withMetadata(new ObjectMetaBuilder()
+ .withName(TEST_RESOURCE_NAME)
+ .build())
+ .withData(Map.of(KEY, "val"))
+ .build();
+
+ extension.create(cm);
+ extension.create(testResource());
+
+ await().pollDelay(Duration.ofMillis(200)).untilAsserted(() -> {
+ var currentCM = extension.get(ConfigMap.class, TEST_RESOURCE_NAME);
+ assertThat(currentCM.getData()).containsKey(KEY);
+ });
+ }
+
+ CreateOnlyIfNotExistingDependentWithSSACustomResource testResource() {
+ var res = new CreateOnlyIfNotExistingDependentWithSSACustomResource();
+ res.setMetadata(new ObjectMetaBuilder()
+ .withName(TEST_RESOURCE_NAME)
+ .build());
+
+ return res;
+ }
+
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentSSAMigrationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentSSAMigrationIT.java
index 7e226eca38..0eabc07c97 100644
--- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentSSAMigrationIT.java
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DependentSSAMigrationIT.java
@@ -144,9 +144,10 @@ private DependnetSSACustomResource reconcileWithLegacyOperator(Operator legacyOp
private Operator createOperator(KubernetesClient client, boolean legacyDependentHandling,
String fieldManager) {
Operator operator = new Operator(client,
- o -> o.withSSABasedCreateUpdateMatchForDependentResources(!legacyDependentHandling)
- .withCloseClientOnStop(false));
- operator.register(new DependentSSAReconciler(), o -> {
+ o -> o.withCloseClientOnStop(false));
+ var reconciler = new DependentSSAReconciler(!legacyDependentHandling);
+ reconciler.setKubernetesClient(client);
+ operator.register(reconciler, o -> {
o.settingNamespace(namespace);
if (fieldManager != null) {
o.withFieldManager(fieldManager);
@@ -155,7 +156,6 @@ private Operator createOperator(KubernetesClient client, boolean legacyDependent
return operator;
}
-
public DependnetSSACustomResource testResource() {
DependnetSSACustomResource resource = new DependnetSSACustomResource();
resource.setMetadata(new ObjectMetaBuilder()
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatefulSetDesiredSanitizerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatefulSetDesiredSanitizerIT.java
new file mode 100644
index 0000000000..5313fb7dfb
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatefulSetDesiredSanitizerIT.java
@@ -0,0 +1,57 @@
+package io.javaoperatorsdk.operator;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.apps.StatefulSet;
+import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
+import io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer.StatefulSetDesiredSanitizerCustomResource;
+import io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer.StatefulSetDesiredSanitizerDependentResource;
+import io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer.StatefulSetDesiredSanitizerReconciler;
+import io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer.StatefulSetDesiredSanitizerSpec;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+public class StatefulSetDesiredSanitizerIT {
+
+ public static final String TEST_1 = "test1";
+
+ @RegisterExtension
+ LocallyRunOperatorExtension extension =
+ LocallyRunOperatorExtension.builder()
+ .withReconciler(new StatefulSetDesiredSanitizerReconciler())
+ .build();
+
+ @Test
+ void testSSAMatcher() {
+ var resource = extension.create(testResource());
+
+ await().pollDelay(Duration.ofMillis(200)).untilAsserted(() -> {
+ var statefulSet = extension.get(StatefulSet.class, TEST_1);
+ assertThat(statefulSet).isNotNull();
+ });
+ // make sure reconciliation happens at least once more
+ resource.getSpec().setValue("changed value");
+ extension.replace(resource);
+
+ await().untilAsserted(
+ () -> assertThat(StatefulSetDesiredSanitizerDependentResource.nonMatchedAtLeastOnce)
+ .isFalse());
+ }
+
+ StatefulSetDesiredSanitizerCustomResource testResource() {
+ var res = new StatefulSetDesiredSanitizerCustomResource();
+ res.setMetadata(new ObjectMetaBuilder()
+ .withName(TEST_1)
+ .build());
+ res.setSpec(new StatefulSetDesiredSanitizerSpec());
+ res.getSpec().setValue("initial value");
+
+ return res;
+ }
+
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createonlyifnotexistsdependentwithssa/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createonlyifnotexistsdependentwithssa/ConfigMapDependentResource.java
new file mode 100644
index 0000000000..d6947c2834
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createonlyifnotexistsdependentwithssa/ConfigMapDependentResource.java
@@ -0,0 +1,30 @@
+package io.javaoperatorsdk.operator.sample.createonlyifnotexistsdependentwithssa;
+
+import java.util.Map;
+
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+
+public class ConfigMapDependentResource extends
+ CRUDKubernetesDependentResource {
+
+ public ConfigMapDependentResource() {
+ super(ConfigMap.class);
+ }
+
+ @Override
+ protected ConfigMap desired(CreateOnlyIfNotExistingDependentWithSSACustomResource primary,
+ Context context) {
+ ConfigMap configMap = new ConfigMap();
+ configMap.setMetadata(new ObjectMetaBuilder()
+ .withName(primary.getMetadata().getName())
+ .withNamespace(primary.getMetadata().getNamespace())
+ .build());
+ configMap.setData(Map.of("drkey", "v"));
+ return configMap;
+ }
+}
+
+
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSACustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSACustomResource.java
new file mode 100644
index 0000000000..23f06c365f
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSACustomResource.java
@@ -0,0 +1,13 @@
+package io.javaoperatorsdk.operator.sample.createonlyifnotexistsdependentwithssa;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Version;
+
+@Group("sample.javaoperatorsdk")
+@Version("v1")
+public class CreateOnlyIfNotExistingDependentWithSSACustomResource
+ extends CustomResource
+ implements Namespaced {
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSAReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSAReconciler.java
new file mode 100644
index 0000000000..884b5a859d
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createonlyifnotexistsdependentwithssa/CreateOnlyIfNotExistingDependentWithSSAReconciler.java
@@ -0,0 +1,32 @@
+package io.javaoperatorsdk.operator.sample.createonlyifnotexistsdependentwithssa;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+
+@ControllerConfiguration(dependents = {
+ @Dependent(type = ConfigMapDependentResource.class)})
+public class CreateOnlyIfNotExistingDependentWithSSAReconciler
+ implements Reconciler {
+
+ private final AtomicInteger numberOfExecutions = new AtomicInteger(0);
+
+ @Override
+ public UpdateControl reconcile(
+ CreateOnlyIfNotExistingDependentWithSSACustomResource resource,
+ Context context) {
+ numberOfExecutions.addAndGet(1);
+ return UpdateControl.noUpdate();
+ }
+
+ public int getNumberOfExecutions() {
+ return numberOfExecutions.get();
+ }
+
+
+
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java
index ef2ddfc4ec..6c38b2dd09 100644
--- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java
@@ -16,7 +16,7 @@
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.junit.KubernetesClientAware;
-import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
@@ -26,10 +26,35 @@ public class CreateUpdateEventFilterTestReconciler
EventSourceInitializer,
KubernetesClientAware {
+ private static final class DirectConfigMapDependentResource
+ extends
+ CRUDKubernetesDependentResource {
+
+ private ConfigMap desired;
+
+ private DirectConfigMapDependentResource(Class resourceType) {
+ super(resourceType);
+ }
+
+ @Override
+ protected ConfigMap desired(CreateUpdateEventFilterTestCustomResource primary,
+ Context context) {
+ return desired;
+ }
+
+ @Override
+ public void setEventSource(
+ io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource eventSource) {
+ super.setEventSource(eventSource);
+ }
+ }
+
public static final String CONFIG_MAP_TEST_DATA_KEY = "key";
private KubernetesClient client;
private final AtomicInteger numberOfExecutions = new AtomicInteger(0);
private InformerEventSource informerEventSource;
+ private DirectConfigMapDependentResource configMapDR =
+ new DirectConfigMapDependentResource(ConfigMap.class);
@Override
public UpdateControl reconcile(
@@ -44,43 +69,14 @@ public UpdateControl reconcile(
.withName(resource.getMetadata().getName())
.get();
if (configMap == null) {
- var configMapToCreate = createConfigMap(resource);
- final var resourceID = ResourceID.fromResource(configMapToCreate);
- try {
- informerEventSource.prepareForCreateOrUpdateEventFiltering(resourceID, configMapToCreate);
- configMap =
- client
- .configMaps()
- .inNamespace(resource.getMetadata().getNamespace())
- .resource(configMapToCreate)
- .create();
- informerEventSource.handleRecentResourceCreate(resourceID, configMap);
- } catch (RuntimeException e) {
- informerEventSource
- .cleanupOnCreateOrUpdateEventFiltering(resourceID);
- throw e;
- }
+ configMapDR.desired = createConfigMap(resource);
+ configMapDR.reconcile(resource, context);
} else {
- ResourceID resourceID = ResourceID.fromResource(configMap);
if (!Objects.equals(
configMap.getData().get(CONFIG_MAP_TEST_DATA_KEY), resource.getSpec().getValue())) {
configMap.getData().put(CONFIG_MAP_TEST_DATA_KEY, resource.getSpec().getValue());
- try {
- informerEventSource
- .prepareForCreateOrUpdateEventFiltering(resourceID, configMap);
- var newConfigMap =
- client
- .configMaps()
- .inNamespace(resource.getMetadata().getNamespace())
- .resource(configMap)
- .replace();
- informerEventSource.handleRecentResourceUpdate(resourceID,
- newConfigMap, configMap);
- } catch (RuntimeException e) {
- informerEventSource
- .cleanupOnCreateOrUpdateEventFiltering(resourceID);
- throw e;
- }
+ configMapDR.desired = configMap;
+ configMapDR.reconcile(resource, context);
}
}
return UpdateControl.noUpdate();
@@ -109,6 +105,10 @@ public Map