diff --git a/caffeine-bounded-cache-support/pom.xml b/caffeine-bounded-cache-support/pom.xml index f5d75f2321..a1b3e68470 100644 --- a/caffeine-bounded-cache-support/pom.xml +++ b/caffeine-bounded-cache-support/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/micrometer-support/pom.xml b/micrometer-support/pom.xml index 9372c9700d..a418291dcc 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/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-bom/pom.xml b/operator-framework-bom/pom.xml index f65cc8f119..d3036a6079 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk operator-framework-bom - 4.4.4-SNAPSHOT + 4.5.0-SNAPSHOT Operator SDK - Bill of Materials pom Java SDK for implementing Kubernetes operators diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index d62d84ceaa..792c3c464b 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 4.4.4-SNAPSHOT + 4.5.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/LeaderElectionManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/LeaderElectionManager.java index 1509d87f2a..316fdb4524 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/LeaderElectionManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/LeaderElectionManager.java @@ -66,7 +66,7 @@ private void init(LeaderElectionConfiguration config) { config.getLeaseDuration(), config.getRenewDeadline(), config.getRetryPeriod(), - leaderCallbacks(), + leaderCallbacks(config), true, config.getLeaseName())) .build(); @@ -74,11 +74,20 @@ private void init(LeaderElectionConfiguration config) { - private LeaderCallbacks leaderCallbacks() { + private LeaderCallbacks leaderCallbacks(LeaderElectionConfiguration config) { return new LeaderCallbacks( - this::startLeading, - this::stopLeading, - leader -> log.info("New leader with identity: {}", leader)); + () -> { + config.getLeaderCallbacks().ifPresent(LeaderCallbacks::onStartLeading); + LeaderElectionManager.this.startLeading(); + }, + () -> { + config.getLeaderCallbacks().ifPresent(LeaderCallbacks::onStopLeading); + LeaderElectionManager.this.stopLeading(); + }, + leader -> { + config.getLeaderCallbacks().ifPresent(cb -> cb.onNewLeader(leader)); + log.info("New leader with identity: {}", leader); + }); } private void startLeading() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java index ae836f2a72..d908f52ebe 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java @@ -230,6 +230,7 @@ private static List 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> 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 deletePostcondition() default Condition.class; + Class 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> 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 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 onAddFilter = null; OnUpdateFilter onUpdateFilter = null; OnDeleteFilter onDeleteFilter = null; GenericFilter 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: - *

- * - * @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 prepareEventSources( .build(); informerEventSource = new InformerEventSource<>(informerConfiguration, client); + + this.configMapDR.setKubernetesClient(context.getClient()); + this.configMapDR.setEventSource(informerEventSource); + return EventSourceInitializer.nameEventSources(informerEventSource); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentssa/DependentSSAReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentssa/DependentSSAReconciler.java index d5c48256f7..f84dfe4597 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentssa/DependentSSAReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentssa/DependentSSAReconciler.java @@ -1,21 +1,43 @@ package io.javaoperatorsdk.operator.sample.dependentssa; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfigBuilder; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -@ControllerConfiguration(dependents = {@Dependent(type = SSAConfigMapDependent.class)}) +@ControllerConfiguration public class DependentSSAReconciler - implements Reconciler, TestExecutionInfoProvider { + implements Reconciler, TestExecutionInfoProvider, + KubernetesClientAware, + EventSourceInitializer { private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private SSAConfigMapDependent ssaConfigMapDependent = new SSAConfigMapDependent(); + private KubernetesClient kubernetesClient; + + public DependentSSAReconciler() { + this(true); + } + + public DependentSSAReconciler(boolean useSSA) { + ssaConfigMapDependent.configureWith(new KubernetesDependentResourceConfigBuilder() + .withUseSSA(useSSA) + .build()); + } + @Override public UpdateControl reconcile( DependnetSSACustomResource resource, Context context) { + + ssaConfigMapDependent.reconcile(resource, context); numberOfExecutions.addAndGet(1); return UpdateControl.noUpdate(); } @@ -24,4 +46,21 @@ public int getNumberOfExecutions() { return numberOfExecutions.get(); } + @Override + public KubernetesClient getKubernetesClient() { + return kubernetesClient; + } + + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + ssaConfigMapDependent.setKubernetesClient(kubernetesClient); + } + + @Override + public Map prepareEventSources( + EventSourceContext context) { + return EventSourceInitializer.nameEventSourcesFromDependentResource(context, + ssaConfigMapDependent); + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentssa/SSAConfigMapDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentssa/SSAConfigMapDependent.java index 70f0ddcf15..806eccd717 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentssa/SSAConfigMapDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentssa/SSAConfigMapDependent.java @@ -33,10 +33,10 @@ protected ConfigMap desired(DependnetSSACustomResource primary, } @Override - public ConfigMap update(ConfigMap actual, ConfigMap target, + public ConfigMap update(ConfigMap actual, ConfigMap desired, DependnetSSACustomResource primary, Context context) { NUMBER_OF_UPDATES.incrementAndGet(); - return super.update(actual, target, primary, context); + return super.update(actual, desired, primary, context); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/indexdiscriminator/IndexDiscriminator.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/indexdiscriminator/IndexDiscriminator.java deleted file mode 100644 index eb6e193479..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/indexdiscriminator/IndexDiscriminator.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.javaoperatorsdk.operator.sample.indexdiscriminator; - -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; - -import static io.javaoperatorsdk.operator.sample.indexdiscriminator.IndexDiscriminatorTestReconciler.configMapKeyFromPrimary; - -public class IndexDiscriminator - implements ResourceDiscriminator { - - private final String indexName; - private final String nameSuffix; - - public IndexDiscriminator(String indexName, String nameSuffix) { - this.indexName = indexName; - this.nameSuffix = nameSuffix; - } - - @Override - public Optional distinguish(Class resource, - IndexDiscriminatorTestCustomResource primary, - Context context) { - - InformerEventSource eventSource = - (InformerEventSource) context - .eventSourceRetriever() - .getResourceEventSourceFor(ConfigMap.class); - var resources = eventSource.byIndex(indexName, configMapKeyFromPrimary(primary, nameSuffix)); - if (resources.isEmpty()) { - return Optional.empty(); - } else if (resources.size() > 1) { - throw new IllegalStateException("more than one resource"); - } else { - return Optional.of(resources.get(0)); - } - } -} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/indexdiscriminator/IndexDiscriminatorTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/indexdiscriminator/IndexDiscriminatorTestReconciler.java index 0b0af2a1cc..b988c93491 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/indexdiscriminator/IndexDiscriminatorTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/indexdiscriminator/IndexDiscriminatorTestReconciler.java @@ -81,10 +81,10 @@ public Map prepareEventSources( firstDependentResourceConfigMap .setResourceDiscriminator( - new IndexDiscriminator(CONFIG_MAP_INDEX_1, FIRST_CONFIG_MAP_SUFFIX_1)); + new TestIndexDiscriminator(CONFIG_MAP_INDEX_1, FIRST_CONFIG_MAP_SUFFIX_1)); secondDependentResourceConfigMap .setResourceDiscriminator( - new IndexDiscriminator(CONFIG_MAP_INDEX_2, FIRST_CONFIG_MAP_SUFFIX_2)); + new TestIndexDiscriminator(CONFIG_MAP_INDEX_2, FIRST_CONFIG_MAP_SUFFIX_2)); return EventSourceInitializer.nameEventSources(eventSource); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/indexdiscriminator/TestIndexDiscriminator.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/indexdiscriminator/TestIndexDiscriminator.java new file mode 100644 index 0000000000..a56e44ced8 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/indexdiscriminator/TestIndexDiscriminator.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.sample.indexdiscriminator; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.reconciler.IndexDiscriminator; + +import static io.javaoperatorsdk.operator.sample.indexdiscriminator.IndexDiscriminatorTestReconciler.configMapKeyFromPrimary; + +public class TestIndexDiscriminator + extends IndexDiscriminator { + + public TestIndexDiscriminator(String indexName, String nameSuffix) { + super(indexName, p -> configMapKeyFromPrimary(p, nameSuffix)); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primaryindexer/DependentPrimaryIndexerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primaryindexer/DependentPrimaryIndexerTestReconciler.java index 6d79d4ee56..e123500c42 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primaryindexer/DependentPrimaryIndexerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primaryindexer/DependentPrimaryIndexerTestReconciler.java @@ -12,8 +12,8 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; @ControllerConfiguration(dependents = @Dependent( type = DependentPrimaryIndexerTestReconciler.ReadOnlyConfigMapDependent.class)) @@ -39,7 +39,7 @@ public Set toPrimaryResourceIDs(ConfigMap dependentResource) { } @Override - public Optional> eventSource( + public Optional> eventSource( EventSourceContext context) { cache = context.getPrimaryCache(); cache.addIndexer(CONFIG_MAP_RELATION_INDEXER, indexer); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/servicestrictmatcher/ServiceDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/servicestrictmatcher/ServiceDependentResource.java index 1079642b33..deddf20263 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/servicestrictmatcher/ServiceDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/servicestrictmatcher/ServiceDependentResource.java @@ -53,10 +53,10 @@ public Matcher.Result match(Service actualResource, } @Override - public Service update(Service actual, Service target, + public Service update(Service actual, Service desired, ServiceStrictMatcherTestCustomResource primary, Context context) { updated.addAndGet(1); - return super.update(actual, target, primary, context); + return super.update(actual, desired, primary, context); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/ssalegacymatcher/ServiceDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/ssalegacymatcher/ServiceDependentResource.java index 5230404ceb..a1f5f6faf0 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/ssalegacymatcher/ServiceDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/ssalegacymatcher/ServiceDependentResource.java @@ -45,17 +45,17 @@ public Result match(Service actualResource, SSALegacyMatcherCustomResou // override just to check the exec count @Override - public Service update(Service actual, Service target, SSALegacyMatcherCustomResource primary, + public Service update(Service actual, Service desired, SSALegacyMatcherCustomResource primary, Context context) { createUpdateCount.addAndGet(1); - return super.update(actual, target, primary, context); + return super.update(actual, desired, primary, context); } // override just to check the exec count @Override - public Service create(Service target, SSALegacyMatcherCustomResource primary, + public Service create(Service desired, SSALegacyMatcherCustomResource primary, Context context) { createUpdateCount.addAndGet(1); - return super.create(target, primary, context); + return super.create(desired, primary, context); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerCustomResource.java new file mode 100644 index 0000000000..bf35304da0 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerCustomResource.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer; + +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 StatefulSetDesiredSanitizerCustomResource + extends CustomResource + implements Namespaced { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java new file mode 100644 index 0000000000..12525c4c8d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerDependentResource.java @@ -0,0 +1,43 @@ +package io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.apps.StatefulSet; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; + +public class StatefulSetDesiredSanitizerDependentResource + extends + CRUDKubernetesDependentResource { + + public static volatile Boolean nonMatchedAtLeastOnce; + + public StatefulSetDesiredSanitizerDependentResource() { + super(StatefulSet.class); + } + + @Override + protected StatefulSet desired(StatefulSetDesiredSanitizerCustomResource primary, + Context context) { + var template = + ReconcilerUtils.loadYaml(StatefulSet.class, getClass(), "statefulset.yaml"); + template.setMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + return template; + } + + @Override + public Result match(StatefulSet actualResource, + StatefulSetDesiredSanitizerCustomResource primary, + Context context) { + var res = super.match(actualResource, primary, context); + if (!res.matched()) { + nonMatchedAtLeastOnce = true; + } else if (nonMatchedAtLeastOnce == null) { + nonMatchedAtLeastOnce = false; + } + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerReconciler.java new file mode 100644 index 0000000000..c884619227 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerReconciler.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer; + +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 = StatefulSetDesiredSanitizerDependentResource.class)}) +public class StatefulSetDesiredSanitizerReconciler + implements Reconciler { + + @Override + public UpdateControl reconcile( + StatefulSetDesiredSanitizerCustomResource resource, + Context context) throws Exception { + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerSpec.java new file mode 100644 index 0000000000..3cca134108 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/StatefulSetDesiredSanitizerSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.statefulsetdesiredsanitizer; + +public class StatefulSetDesiredSanitizerSpec { + + private String value; + + public String getValue() { + return value; + } + + public StatefulSetDesiredSanitizerSpec setValue(String value) { + this.value = value; + return this; + } +} diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/statefulset.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/statefulset.yaml new file mode 100644 index 0000000000..f40fbeb607 --- /dev/null +++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/statefulsetdesiredsanitizer/statefulset.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: "" +spec: + selector: + matchLabels: + app: nginx # has to match .spec.template.metadata.labels + serviceName: "nginx" + replicas: 1 + minReadySeconds: 10 # by default is 0 + template: + metadata: + labels: + app: nginx # has to match .spec.selector.matchLabels + spec: + terminationGracePeriodSeconds: 10 + containers: + - name: nginx + image: registry.k8s.io/nginx-slim:0.8 + ports: + - containerPort: 80 + name: web + volumeMounts: + - name: www + mountPath: /usr/share/nginx/html + volumeClaimTemplates: + - metadata: + name: www + spec: + accessModes: [ "ReadWriteOnce" ] + storageClassName: "my-storage-class" + resources: + requests: + storage: 1Gi \ No newline at end of file diff --git a/pom.xml b/pom.xml index 32a8134f6d..b9c04ebfba 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 4.4.4-SNAPSHOT + 4.5.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index 0cf0452a2a..4bcac68af5 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.4.4-SNAPSHOT + 4.5.0-SNAPSHOT sample-leader-election diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 71eddaf328..25f474679c 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.4.4-SNAPSHOT + 4.5.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 61807ada45..61e1322f65 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 4.4.4-SNAPSHOT + 4.5.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 8eae54d73e..e68aa8603a 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.4.4-SNAPSHOT + 4.5.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 59109cc13e..aef20d77d7 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.4.4-SNAPSHOT + 4.5.0-SNAPSHOT sample-webpage-operator diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java index 1aaf6d19db..d21e89a172 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java @@ -17,7 +17,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; 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.Workflow; import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowBuilder; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -91,8 +91,8 @@ private void initDependentResources(KubernetesClient client) { Arrays.asList(configMapDR, deploymentDR, serviceDR, ingressDR).forEach(dr -> { dr.setKubernetesClient(client); - dr.configureWith(new KubernetesDependentResourceConfig() - .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + dr.configureWith(new KubernetesDependentResourceConfigBuilder() + .withLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR).build()); }); } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java index 3230259c1d..c1b3030c9b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java @@ -13,7 +13,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.reconciler.*; 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.event.source.EventSource; import io.javaoperatorsdk.operator.sample.customresource.WebPage; import io.javaoperatorsdk.operator.sample.dependentresource.ConfigMapDependentResource; @@ -79,21 +79,19 @@ public ErrorStatusUpdateControl updateErrorStatus( return handleError(resource, e); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) private void createDependentResources(KubernetesClient client) { this.configMapDR = new ConfigMapDependentResource(); this.deploymentDR = new DeploymentDependentResource(); this.serviceDR = new ServiceDependentResource(); this.ingressDR = new IngressDependentResource(); - Arrays.asList(configMapDR, deploymentDR, serviceDR, ingressDR).forEach(dr -> { dr.setKubernetesClient(client); - dr.configureWith(new KubernetesDependentResourceConfig() - .setLabelSelector(SELECTOR + "=true")); + dr.configureWith(new KubernetesDependentResourceConfigBuilder() + .withLabelSelector(SELECTOR + "=true").build()); }); } - } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ConfigMapDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ConfigMapDependentResource.java index cc7b15146b..8641aa343b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ConfigMapDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/dependentresource/ConfigMapDependentResource.java @@ -3,9 +3,6 @@ import java.util.HashMap; import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; @@ -15,7 +12,6 @@ import io.javaoperatorsdk.operator.sample.customresource.WebPage; import static io.javaoperatorsdk.operator.sample.Utils.configMapName; -import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; import static io.javaoperatorsdk.operator.sample.WebPageManagedDependentsReconciler.SELECTOR; // this annotation only activates when using managed dependents and is not otherwise needed @@ -23,8 +19,6 @@ public class ConfigMapDependentResource extends CRUDKubernetesDependentResource { - private static final Logger log = LoggerFactory.getLogger(ConfigMapDependentResource.class); - public ConfigMapDependentResource() { super(ConfigMap.class); } @@ -45,22 +39,4 @@ protected ConfigMap desired(WebPage webPage, Context context) { .withData(data) .build(); } - - @Override - public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, - Context context) { - var res = super.update(actual, target, primary, context); - var ns = actual.getMetadata().getNamespace(); - log.info("Restarting pods because HTML has changed in {}", - ns); - // not that this is not necessary, eventually mounted config map would be updated, just this way - // is much faster; what is handy for demo purposes. - // https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#mounted-configmaps-are-updated-automatically - getKubernetesClient() - .pods() - .inNamespace(ns) - .withLabel("app", deploymentName(primary)) - .delete(); - return res; - } } diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java index 24945a58b3..d1445e34ba 100644 --- a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java @@ -36,6 +36,7 @@ public abstract class WebPageOperatorAbstractTest { public static final String TITLE1 = "Hello Operator World"; public static final String TITLE2 = "Hello Operator World Title 2"; public static final int WAIT_SECONDS = 20; + public static final int LONG_WAIT_SECONDS = 120; public static final Duration POLL_INTERVAL = Duration.ofSeconds(1); boolean isLocal() { @@ -68,7 +69,7 @@ void testAddingWebPage() { // update part: changing title operator().replace(createWebPage(TITLE2)); - await().atMost(Duration.ofSeconds(WAIT_SECONDS)) + await().atMost(Duration.ofSeconds(LONG_WAIT_SECONDS)) .pollInterval(POLL_INTERVAL) .untilAsserted(() -> { String page = httpGetForWebPage(webPage);