Skip to content

Commit b419155

Browse files
committed
test: add tests for graceful handling of null updates and finalizer removal
Signed-off-by: David Sondermann <david.sondermann@hivemq.com>
1 parent bfd2ea7 commit b419155

2 files changed

Lines changed: 94 additions & 18 deletions

File tree

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperationsTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@
2525
import io.fabric8.kubernetes.client.KubernetesClient;
2626
import io.fabric8.kubernetes.client.KubernetesClientException;
2727
import io.fabric8.kubernetes.client.dsl.MixedOperation;
28+
import io.fabric8.kubernetes.client.dsl.NamespaceableResource;
2829
import io.fabric8.kubernetes.client.dsl.Resource;
30+
import io.javaoperatorsdk.operator.MockKubernetesClient;
2931
import io.javaoperatorsdk.operator.TestUtils;
32+
import io.javaoperatorsdk.operator.api.config.BaseConfigurationService;
3033
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
34+
import io.javaoperatorsdk.operator.api.config.ResolvedControllerConfiguration;
35+
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
36+
import io.javaoperatorsdk.operator.processing.Controller;
3137
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
3238
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
3339
import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource;
@@ -247,6 +253,39 @@ void retriesFinalizerRemovalWithFreshResource() {
247253
verify(resourceOp, times(1)).get();
248254
}
249255

256+
@Test
257+
void removeFinalizerHandlesAlreadyDeletedTargetGracefully() {
258+
// When the underlying patch returns null because the target resource is already gone
259+
// on the API server (fabric8's edit() returns null on 404), removeFinalizer must
260+
// return cleanly rather than throw
261+
var resource = TestUtils.testCustomResource1();
262+
resource.getMetadata().setResourceVersion("1");
263+
resource.addFinalizer(FINALIZER_NAME);
264+
when(context.getPrimaryResource()).thenReturn(resource);
265+
266+
// Real ControllerEventSource so eventFilteringUpdateAndCacheResource and the
267+
// TemporaryResourceCache it talks to are wired up the way they would be in production
268+
NamespaceableResource<TestCustomResource> single = mock();
269+
when(single.edit(any(UnaryOperator.class))).thenReturn(null);
270+
var client = MockKubernetesClient.client(TestCustomResource.class);
271+
when(client.resource(any(TestCustomResource.class))).thenReturn(single);
272+
273+
var eventSource = new ControllerEventSource<>(testController(client));
274+
eventSource.start();
275+
try {
276+
when(context.getClient()).thenReturn(client);
277+
var eventSourceRetriever = mock(EventSourceRetriever.class);
278+
when(eventSourceRetriever.getControllerEventSource()).thenReturn(eventSource);
279+
when(context.eventSourceRetriever()).thenReturn(eventSourceRetriever);
280+
281+
var result = resourceOperations.removeFinalizer(FINALIZER_NAME);
282+
283+
assertThat(result).isNull();
284+
} finally {
285+
eventSource.stop();
286+
}
287+
}
288+
250289
@Test
251290
void resourcePatchWithSingleEventSource() {
252291
var resource = TestUtils.testCustomResource1();
@@ -324,4 +363,28 @@ void resourcePatchThrowsWhenEventSourceIsNotManagedInformer() {
324363
assertThat(exception.getMessage()).contains("Target event source must be a subclass off");
325364
assertThat(exception.getMessage()).contains("ManagedInformerEventSource");
326365
}
366+
367+
private Controller<TestCustomResource> testController(KubernetesClient client) {
368+
var informerConfig =
369+
InformerConfiguration.builder(TestCustomResource.class)
370+
.withComparableResourceVersions(true)
371+
.withGhostResourceCacheCheckInterval(Constants.DEFAULT_GHOST_RESOURCE_CHECK_INTERVAL)
372+
.buildForController();
373+
var configuration =
374+
new ResolvedControllerConfiguration<>(
375+
"test",
376+
true,
377+
null,
378+
null,
379+
null,
380+
null,
381+
FINALIZER_NAME,
382+
null,
383+
null,
384+
new BaseConfigurationService(),
385+
informerConfig,
386+
false,
387+
null);
388+
return new Controller<>((r, ctx) -> UpdateControl.noUpdate(), configuration, client);
389+
}
327390
}

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,19 @@ void ghostCheckRunsConcurrentlyWithPutResource() {
412412
ghostCheckExecutor.shutdownNow();
413413
}
414414

415+
@Test
416+
void eventFilteringUpdateHandlesNullFromUpdateMethodGracefully() {
417+
// When the supplied update operation returns null (e.g. fabric8's edit() returns null
418+
// because the patch target is already gone on the API server), null must propagate
419+
// cleanly out of eventFilteringUpdateAndCacheResource rather than throw.
420+
withRealTemporaryResourceCache();
421+
422+
var result =
423+
informerEventSource.eventFilteringUpdateAndCacheResource(testDeployment(), r -> null);
424+
425+
assertThat(result).isNull();
426+
}
427+
415428
@Test
416429
void filteringUpdateAndGhostCheckWithNamespaceChange() {
417430
var mes = mock(ManagedInformerEventSource.class);
@@ -461,26 +474,26 @@ private void assertNoEventProduced() {
461474
private void expectHandleEvent(int newResourceVersion, int oldResourceVersion) {
462475
await()
463476
.untilAsserted(
464-
() -> {
465-
verify(informerEventSource, times(1))
466-
.handleEvent(
467-
eq(ResourceAction.UPDATED),
468-
argThat(
469-
newResource -> {
470-
assertThat(newResource.getMetadata().getResourceVersion())
471-
.isEqualTo("" + newResourceVersion);
472-
return true;
473-
}),
474-
argThat(
475-
newResource -> {
476-
assertThat(newResource.getMetadata().getResourceVersion())
477-
.isEqualTo("" + oldResourceVersion);
478-
return true;
479-
}),
480-
isNull());
481-
});
477+
() ->
478+
verify(informerEventSource, times(1))
479+
.handleEvent(
480+
eq(ResourceAction.UPDATED),
481+
argThat(
482+
newResource -> {
483+
assertThat(newResource.getMetadata().getResourceVersion())
484+
.isEqualTo("" + newResourceVersion);
485+
return true;
486+
}),
487+
argThat(
488+
newResource -> {
489+
assertThat(newResource.getMetadata().getResourceVersion())
490+
.isEqualTo("" + oldResourceVersion);
491+
return true;
492+
}),
493+
isNull()));
482494
}
483495

496+
@SuppressWarnings("SameParameterValue")
484497
private CountDownLatch sendForEventFilteringUpdate(int resourceVersion) {
485498
return sendForEventFilteringUpdate(testDeployment(), resourceVersion);
486499
}

0 commit comments

Comments
 (0)