Skip to content

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.Ignore;
import io.javaoperatorsdk.operator.api.reconciler.ResourceOperations;
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ConfiguredDependentResource;
import io.javaoperatorsdk.operator.processing.GroupVersionKind;
Expand Down Expand Up @@ -90,10 +91,12 @@ public R create(R desired, P primary, Context<P> context) {
desired.getClass(),
ResourceID.fromResource(desired),
ssa);

return ssa
? context.resourceOperations().serverSideApply(desired, eventSource().orElse(null))
: context.resourceOperations().create(desired, eventSource().orElse(null));
: context
.resourceOperations()
.create(
desired, eventSource().orElse(null), ResourceOperations.Options.forcedFiltering());
}

public R update(R actual, R desired, P primary, Context<P> context) {
Expand All @@ -114,7 +117,12 @@ public R update(R actual, R desired, P primary, Context<P> context) {
ssa);
if (ssa) {
updatedResource =
context.resourceOperations().serverSideApply(desired, eventSource().orElse(null));
context
.resourceOperations()
.serverSideApply(
desired,
eventSource().orElse(null),
ResourceOperations.Options.forcedFiltering());
} else {
var updatedActual = GenericResourceUpdater.updateResource(actual, desired, context);
updatedResource =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,7 @@ private PostExecutionControl<P> handleReconcile(
} else {
updatedResource = context.resourceOperations().addFinalizer();
}
return PostExecutionControl.onlyFinalizerAdded(updatedResource)
.withReSchedule(BaseControl.INSTANT_RESCHEDULE);
return PostExecutionControl.onlyFinalizerAdded(updatedResource);
} else {
try {
return reconcileExecution(executionScope, resourceForExecution, originalResource, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.config.Informable;
import io.javaoperatorsdk.operator.api.config.NamespaceChangeable;
import io.javaoperatorsdk.operator.api.reconciler.Experimental;
import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller;
import io.javaoperatorsdk.operator.health.InformerHealthIndicator;
import io.javaoperatorsdk.operator.health.InformerWrappingEventSourceHealthIndicator;
Expand All @@ -48,6 +49,8 @@
import io.javaoperatorsdk.operator.processing.event.source.*;
import io.javaoperatorsdk.operator.processing.event.source.ResourceAction;

import static io.javaoperatorsdk.operator.api.reconciler.Experimental.API_MIGHT_CHANGE;

@SuppressWarnings("rawtypes")
public abstract class ManagedInformerEventSource<
R extends HasMetadata, P extends HasMetadata, C extends Informable<R>>
Expand Down Expand Up @@ -88,13 +91,25 @@ public void changeNamespaces(Set<String> namespaces) {
}
}

@Experimental(API_MIGHT_CHANGE)
public R eventFilteringUpdateAndCacheResource(R resourceToUpdate, UnaryOperator<R> updateMethod) {
return eventFilteringUpdateAndCacheResource(resourceToUpdate, updateMethod, false);
}

/**
* Updates the resource and makes sure that the response is available for the next reconciliation.
* Also makes sure that the even produced by this update is filtered, thus does not trigger the
* reconciliation.
*/
@Experimental(API_MIGHT_CHANGE)
@SuppressWarnings("unchecked")
public R eventFilteringUpdateAndCacheResource(R resourceToUpdate, UnaryOperator<R> updateMethod) {
public R eventFilteringUpdateAndCacheResource(
R resourceToUpdate, UnaryOperator<R> updateMethod, boolean forceUpdateFilter) {
if (resourceToUpdate.getMetadata().getResourceVersion() == null && !forceUpdateFilter) {
log.debug("No resourceVersion set. Skipping event filtering.");
return updateAndCacheResource(resourceToUpdate, updateMethod);
}

ResourceID id = ResourceID.fromResource(resourceToUpdate);
log.debug("Starting event filtering and caching update for id={}", id);
R updatedResource = null;
Expand Down Expand Up @@ -134,6 +149,13 @@ public R eventFilteringUpdateAndCacheResource(R resourceToUpdate, UnaryOperator<
}
}

@Experimental(API_MIGHT_CHANGE)
public R updateAndCacheResource(R resourceToUpdate, UnaryOperator<R> updateOperation) {
var result = updateOperation.apply(resourceToUpdate);
handleRecentResourceUpdate(ResourceID.fromResource(resourceToUpdate), result, resourceToUpdate);
return result;
}

protected abstract void handleEvent(
ResourceAction action,
R resource,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ void addsFinalizer() {

// Mock successful finalizer addition
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
any(), any(UnaryOperator.class)))
any(), any(UnaryOperator.class), anyBoolean()))
.thenAnswer(
invocation -> {
var res = TestUtils.testCustomResource1();
Expand All @@ -99,7 +99,7 @@ void addsFinalizer() {
assertThat(result.hasFinalizer(FINALIZER_NAME)).isTrue();
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("2");
verify(controllerEventSource, times(1))
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class), anyBoolean());
}

@Test
Expand All @@ -111,7 +111,7 @@ void addsFinalizerWithSSA() {

// Mock successful SSA finalizer addition
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
any(), any(UnaryOperator.class)))
any(), any(UnaryOperator.class), anyBoolean()))
.thenAnswer(
invocation -> {
var res = TestUtils.testCustomResource1();
Expand All @@ -126,7 +126,7 @@ void addsFinalizerWithSSA() {
assertThat(result.hasFinalizer(FINALIZER_NAME)).isTrue();
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("2");
verify(controllerEventSource, times(1))
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class), anyBoolean());
}

@Test
Expand All @@ -139,7 +139,7 @@ void removesFinalizer() {

// Mock successful finalizer removal
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
any(), any(UnaryOperator.class)))
any(), any(UnaryOperator.class), anyBoolean()))
.thenAnswer(
invocation -> {
var res = TestUtils.testCustomResource1();
Expand All @@ -154,7 +154,7 @@ void removesFinalizer() {
assertThat(result.hasFinalizer(FINALIZER_NAME)).isFalse();
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("2");
verify(controllerEventSource, times(1))
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class), anyBoolean());
}

@Test
Expand All @@ -166,7 +166,7 @@ void retriesAddingFinalizerWithoutSSA() {

// First call throws conflict, second succeeds
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
any(), any(UnaryOperator.class)))
any(), any(UnaryOperator.class), anyBoolean()))
.thenThrow(new KubernetesClientException("Conflict", 409, null))
.thenAnswer(
invocation -> {
Expand All @@ -184,7 +184,7 @@ void retriesAddingFinalizerWithoutSSA() {
assertThat(result).isNotNull();
assertThat(result.hasFinalizer(FINALIZER_NAME)).isTrue();
verify(controllerEventSource, times(2))
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class), anyBoolean());
verify(resourceOp, times(1)).get();
}

Expand All @@ -198,7 +198,7 @@ void nullResourceIsGracefullyHandledOnFinalizerRemovalRetry() {

// First call throws conflict
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
any(), any(UnaryOperator.class)))
any(), any(UnaryOperator.class), anyBoolean()))
.thenThrow(new KubernetesClientException("Conflict", 409, null));

// Return null on retry (resource was deleted)
Expand All @@ -207,7 +207,7 @@ void nullResourceIsGracefullyHandledOnFinalizerRemovalRetry() {
resourceOperations.removeFinalizer(FINALIZER_NAME);

verify(controllerEventSource, times(1))
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class), anyBoolean());
verify(resourceOp, times(1)).get();
}

Expand All @@ -221,7 +221,7 @@ void retriesFinalizerRemovalWithFreshResource() {

// First call throws unprocessable (422), second succeeds
when(controllerEventSource.eventFilteringUpdateAndCacheResource(
any(), any(UnaryOperator.class)))
any(), any(UnaryOperator.class), anyBoolean()))
.thenThrow(new KubernetesClientException("Unprocessable", 422, null))
.thenAnswer(
invocation -> {
Expand All @@ -243,7 +243,7 @@ void retriesFinalizerRemovalWithFreshResource() {
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("3");
assertThat(result.hasFinalizer(FINALIZER_NAME)).isFalse();
verify(controllerEventSource, times(2))
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class), anyBoolean());
verify(resourceOp, times(1)).get();
}

Expand All @@ -261,15 +261,16 @@ void resourcePatchWithSingleEventSource() {
when(context.eventSourceRetriever()).thenReturn(eventSourceRetriever);
when(eventSourceRetriever.getEventSourcesFor(TestCustomResource.class))
.thenReturn(List.of(managedEventSource));
when(managedEventSource.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class)))
when(managedEventSource.eventFilteringUpdateAndCacheResource(
any(), any(UnaryOperator.class), anyBoolean()))
.thenReturn(updatedResource);

var result = resourceOperations.resourcePatch(resource, UnaryOperator.identity());

assertThat(result).isNotNull();
assertThat(result.getMetadata().getResourceVersion()).isEqualTo("2");
verify(managedEventSource, times(1))
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class), anyBoolean());
}

@Test
Expand Down Expand Up @@ -303,7 +304,7 @@ void resourcePatchUsesFirstEventSourceIfMultipleEventSourcesPresent() {
resourceOperations.resourcePatch(resource, UnaryOperator.identity());

verify(eventSource1, times(1))
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class), anyBoolean());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,47 @@ void filteringUpdateFallsBackToMapperWhenNoPrimaryToSecondaryIndex() {
verify(eventHandlerMock, times(1)).handleEvent(any());
}

@Test
void skipsEventFilteringWhenResourceVersionIsNull() {
// Without a resourceVersion there is nothing to correlate own-write echoes against, so
// event filtering is bypassed: the update is applied and cached directly, no filter window
// is opened and no event is propagated through handleEvent.
var resourceToUpdate = testDeployment();
resourceToUpdate.getMetadata().setResourceVersion(null);
var updated = deploymentWithResourceVersion(3);

var result =
informerEventSource.eventFilteringUpdateAndCacheResource(resourceToUpdate, r -> updated);

assertThat(result).isSameAs(updated);
verify(temporaryResourceCache, times(1)).putResource(updated);
verify(temporaryResourceCache, never()).startEventFilteringModify(any());
verify(temporaryResourceCache, never()).doneEventFilterModify(any());
verify(eventHandlerMock, never()).handleEvent(any());
}

@Test
void forceUpdateFilterOpensFilterWindowEvenWhenResourceVersionIsNull() {
// A write without a resourceVersion (e.g. an SSA finalizer add, which uses no optimistic
// locking) would normally skip event filtering. forceUpdateFilter=true forces filtering
// anyway so the resulting own event is correlated and does not trigger a spurious
// reconciliation.
var resourceToUpdate = testDeployment();
resourceToUpdate.getMetadata().setResourceVersion(null);
var updated = deploymentWithResourceVersion(3);
when(temporaryResourceCache.doneEventFilterModify(any())).thenReturn(Optional.empty());

var result =
informerEventSource.eventFilteringUpdateAndCacheResource(
resourceToUpdate, r -> updated, true);

assertThat(result).isSameAs(updated);
verify(temporaryResourceCache, times(1)).startEventFilteringModify(any());
verify(temporaryResourceCache, times(1)).doneEventFilterModify(any());
verify(temporaryResourceCache, times(1)).putResource(updated);
verify(eventHandlerMock, never()).handleEvent(any());
}

private PrimaryToSecondaryIndex<Deployment> injectIndexMock() throws Exception {
@SuppressWarnings("unchecked")
PrimaryToSecondaryIndex<Deployment> indexMock = mock(PrimaryToSecondaryIndex.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ public UpdateControl<ChangeNamespaceTestCustomResource> reconcile(
ChangeNamespaceTestCustomResource primary,
Context<ChangeNamespaceTestCustomResource> context) {

context.resourceOperations().serverSideApply(configMap(primary));
context
.resourceOperations()
.serverSideApply(configMap(primary), ResourceOperations.Options.forcedFiltering());

if (primary.getStatus() == null) {
primary.setStatus(new ChangeNamespaceTestCustomResourceStatus());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.ResourceOperations;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
Expand Down Expand Up @@ -79,7 +80,11 @@ public UpdateControl<OnRelistFilterCustomResource> reconcile(
if (execution == 1) {
var cm = prepareConfigMap(resource);
switch (mode.get()) {
case NO_RELIST -> context.resourceOperations().serverSideApply(cm, configMapEventSource);
case NO_RELIST ->
context
.resourceOperations()
.serverSideApply(
cm, configMapEventSource, ResourceOperations.Options.forcedFiltering());
case RELIST_AROUND_UPDATE -> {
configMapEventSource.simulateOnBeforeList();
var applied = context.resourceOperations().serverSideApply(cm, configMapEventSource);
Expand All @@ -94,7 +99,10 @@ public UpdateControl<OnRelistFilterCustomResource> reconcile(
case RELIST_COMPLETES_BEFORE_UPDATE -> {
configMapEventSource.simulateOnBeforeList();
configMapEventSource.simulateOnList();
context.resourceOperations().serverSideApply(cm, configMapEventSource);
context
.resourceOperations()
.serverSideApply(
cm, configMapEventSource, ResourceOperations.Options.forcedFiltering());
}
case RELIST_STARTS_DURING_UPDATE -> {
// Drive the event-filtering update path manually so we can fire onBeforeList AFTER the
Expand All @@ -114,7 +122,8 @@ public UpdateControl<OnRelistFilterCustomResource> reconcile(
.withFieldManager(fieldManager)
.withPatchType(PatchType.SERVER_SIDE_APPLY)
.build());
});
},
true);
// See RELIST_AROUND_UPDATE: wait for the own-write event to be buffered while the
// re-list is still in progress, so it is tagged as part of the re-list and propagated.
configMapEventSource.awaitWatchEventReceived(applied);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.ResourceOperations;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
Expand All @@ -50,7 +51,12 @@ public UpdateControl<OwnSecondaryUpdateCustomResource> reconcile(
// version actually advances. With the read-cache-after-write filter in place, none of the
// resulting watch events should trigger a fresh reconciliation.
for (int i = 1; i <= OWN_SSA_COUNT; i++) {
context.resourceOperations().serverSideApply(prepareCM(resource, i), configMapEventSource);
context
.resourceOperations()
.serverSideApply(
prepareCM(resource, i),
configMapEventSource,
ResourceOperations.Options.forcedFiltering());
}
return UpdateControl.noUpdate();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Java Operator SDK Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.javaoperatorsdk.operator.baseapi.readcacheafterwrite.specchangeduringstatuspatch;

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.ShortNames;
import io.fabric8.kubernetes.model.annotation.Version;

@Group("sample.javaoperatorsdk")
@Version("v1")
@ShortNames("scdsp")
public class SpecChangeDuringStatusPatchCustomResource
extends CustomResource<SpecChangeDuringStatusPatchSpec, SpecChangeDuringStatusPatchStatus>
implements Namespaced {}
Loading
Loading