From 1e54fe217766075424ddd89f20b9fcdbe6fdc837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 29 Jul 2026 17:40:35 +0200 Subject: [PATCH] fix: NPE in handleRecentResourceUpdate for an unknown secondary resource `ExternalResourceCachingEventSource.handleRecentResourceUpdate` checks that the primary has an entry in the cache, but then dereferences the per-secondary lookup without checking it: R actualResource = actualValues.get(resourceId); if (actualResource.equals(previousVersionOfResource)) { `actualValues.get(resourceId)` returns null whenever the primary has a cache entry but that particular secondary id is not in it, which throws a NullPointerException. This is reachable from `AbstractEventSourceHolderDependentResource.onUpdated` for any `RecentOperationCacheFiller` event source, e.g. after an update whose resource id is not the one currently cached for that primary. Skips the cache update when the resource is not tracked, which matches the intent of the surrounding "only overwrite if we still hold the version the caller saw" check. Adds a regression test that fails with NullPointerException without this change. --- .../source/ExternalResourceCachingEventSource.java | 2 +- .../ExternalResourceCachingEventSourceTest.java | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java index 8a4c476443..762220d5b1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java @@ -226,7 +226,7 @@ public synchronized void handleRecentResourceUpdate( if (actualValues != null) { var resourceId = resourceIDMapper.idFor(resource); R actualResource = actualValues.get(resourceId); - if (actualResource.equals(previousVersionOfResource)) { + if (actualResource != null && actualResource.equals(previousVersionOfResource)) { actualValues.put(resourceId, resource); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSourceTest.java index 889cc4da75..5b3d0d0e4d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSourceTest.java @@ -211,6 +211,17 @@ void genericFilteringEvents() { verify(eventHandler, times(0)).handleEvent(any()); } + @Test + void recentResourceUpdateIsIgnoredForUnknownSecondaryResource() { + source.handleResources(primaryID1(), Set.of(testResource1())); + + // testResource2 has a different id, so it is not present in the cache for primaryID1 + var unknown = testResource2(); + source.handleRecentResourceUpdate(primaryID1(), unknown, unknown); + + assertThat(source.getSecondaryResources(primaryID1())).containsExactly(testResource1()); + } + public static class TestExternalCachingEventSource extends ExternalResourceCachingEventSource { public TestExternalCachingEventSource() {