Skip to content

Commit 5d5c948

Browse files
committed
wip
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent 7e89ea1 commit 5d5c948

6 files changed

Lines changed: 64 additions & 74 deletions

File tree

docs/content/en/blog/releases/v5-5-release.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ operation type; when one does not fit, supply your own via `Options.matchAndFilt
4343

4444
- `matchAndFilter(...)` / `matchAndFilterWithDefaultMatcher(...)` — match, then write-and-filter only
4545
if needed (the default).
46-
- `filterIfOptimisticLocking()` — filter the own event only when the write uses optimistic locking,
47-
otherwise just cache the response.
46+
- `filterWithOptimisticLocking()` — filter the own event; the write must use optimistic locking,
47+
otherwise an `IllegalArgumentException` is thrown.
4848
- `cacheOnly()` — only cache the response (read-cache-after-write consistency), no filtering.
4949
- `forceFilterEvents()` — always filter (mostly internal usage).
5050

docs/content/en/docs/documentation/reconciler.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,10 @@ third-party write. This requires **either**:
212212
avoids a request to the API server when nothing changed. Default matchers are provided for every
213213
operation type, but they are heuristics — a workflow relying on them should be tested against the
214214
concrete resources it manages, or a custom matcher supplied.
215-
- `Options.filterIfOptimisticLocking()` — filter the own event only when the write uses optimistic
216-
locking, otherwise just cache the response.
215+
- `Options.filterWithOptimisticLocking()` — filter the own event; the write must use optimistic
216+
locking (a resource version set on the written resource), otherwise an `IllegalArgumentException`
217+
is thrown. Requiring optimistic locking guarantees a concurrent third-party change is rejected by
218+
the API server rather than being silently filtered out.
217219
- `Options.cacheOnly()` — only cache the response (read-cache-after-write consistency), no own-event
218220
filtering.
219221
- `Options.forceFilterEvents()` — always filter, regardless of optimistic locking. Only safe when

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperations.java

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@
5050
* selected through {@link Mode}:
5151
*
5252
* <ul>
53-
* <li>{@link Options#filterIfOptimisticLocking()} ({@link Mode#FILTER_IF_OPTIMISTIC_LOCKING}) -
54-
* the own event is filtered only when the write uses optimistic locking (i.e. the resource
55-
* version is set on the resource being written); otherwise the response is only cached. This
56-
* is the safe default for plain update/patch operations.
53+
* <li>{@link Options#filterWithOptimisticLocking()} ({@link Mode#FILTER_WITH_OPTIMISTIC_LOCKING})
54+
* - the own event is filtered; the write must use optimistic locking (i.e. a resource version
55+
* is set on the resource being written), otherwise an {@link IllegalArgumentException} is
56+
* thrown. Requiring optimistic locking guarantees that a concurrent third-party change is
57+
* rejected by the API server rather than silently filtered out.
5758
* <li>{@link Options#matchAndFilter(Matcher)} / {@link
5859
* Options#matchAndFilterWithDefaultMatcher(io.javaoperatorsdk.operator.processing.matcher.UpdateType)}
5960
* ({@link Mode#FILTER_IF_NOT_MATCHING}) - before writing, the desired state is compared to
@@ -860,7 +861,7 @@ public P jsonMergePatchPrimaryStatus(P resource, Options options) {
860861
* Low-level building block behind all the update/patch operations above: runs the given {@code
861862
* updateOperation} against the API server, then caches the response and (depending on the {@link
862863
* Options}) filters the resulting own event. The target event source is resolved automatically
863-
* from the desired resource's type. Uses {@link Options#filterIfOptimisticLocking()}.
864+
* from the desired resource's type. Uses {@link Options#filterWithOptimisticLocking()}.
864865
*
865866
* @param resource the desired resource being written
866867
* @param updateOperation the actual write to perform (update, patch, edit, ...) returning the
@@ -871,7 +872,7 @@ public P jsonMergePatchPrimaryStatus(P resource, Options options) {
871872
* type
872873
*/
873874
public <R extends HasMetadata> R resourcePatch(R resource, UnaryOperator<R> updateOperation) {
874-
return resourcePatch(resource, updateOperation, Options.filterIfOptimisticLocking());
875+
return resourcePatch(resource, updateOperation, Options.filterWithOptimisticLocking());
875876
}
876877

877878
@SuppressWarnings({"rawtypes", "unchecked"})
@@ -917,7 +918,7 @@ private <R extends HasMetadata> R resourcePatch(
917918
@Deprecated(forRemoval = true)
918919
public <R extends HasMetadata> R resourcePatch(
919920
R desired, UnaryOperator<R> updateOperation, ManagedInformerEventSource<R, P, ?> ies) {
920-
return resourcePatch(desired, updateOperation, ies, Options.filterIfOptimisticLocking());
921+
return resourcePatch(desired, updateOperation, ies, Options.filterWithOptimisticLocking());
921922
}
922923

923924
private <R extends HasMetadata> R resourcePatch(
@@ -947,7 +948,9 @@ private <R extends HasMetadata> R resourcePatch(
947948
* @param <R> the resource type
948949
* @return the resource as returned by {@code updateOperation}, or {@code actualResource} when the
949950
* desired state already matches
950-
* @throws IllegalArgumentException if the mode requires a matcher but none is provided
951+
* @throws IllegalArgumentException if the mode requires a matcher but none is provided, or if the
952+
* mode is {@link Mode#FILTER_WITH_OPTIMISTIC_LOCKING} but the resource being written has no
953+
* resource version set
951954
*/
952955
// visible for testing
953956
<R extends HasMetadata> R resourcePatch(
@@ -982,10 +985,16 @@ <R extends HasMetadata> R resourcePatch(
982985
return actualResource;
983986
}
984987

985-
boolean optimisticLocking = desiredResource.getMetadata().getResourceVersion() != null;
988+
if (options.getMode() == Mode.FILTER_WITH_OPTIMISTIC_LOCKING
989+
&& desiredResource.getMetadata().getResourceVersion() == null) {
990+
throw new IllegalArgumentException(
991+
"Mode "
992+
+ Mode.FILTER_WITH_OPTIMISTIC_LOCKING
993+
+ " requires optimistic locking, but no resource version is set on the resource: "
994+
+ ResourceID.fromResource(desiredResource));
995+
}
986996

987-
if (options.getMode() == Mode.CACHE_ONLY
988-
|| (options.getMode() == Mode.FILTER_IF_OPTIMISTIC_LOCKING && !optimisticLocking)) {
997+
if (options.getMode() == Mode.CACHE_ONLY) {
989998
return ies.updateAndCacheResource(desiredResource, updateOperation);
990999
} else {
9911000
return ies.eventFilteringUpdateAndCacheResource(desiredResource, updateOperation);
@@ -1250,8 +1259,8 @@ public P addFinalizerWithSSA(String finalizerName) {
12501259
* is needed for safe own-event filtering), and the trade-offs of the default matchers.
12511260
*
12521261
* <ul>
1253-
* <li>{@link #filterIfOptimisticLocking()} - filter the own event only when the write uses
1254-
* optimistic locking, otherwise only cache the response.
1262+
* <li>{@link #filterWithOptimisticLocking()} - filter the own event; requires the write to use
1263+
* optimistic locking (a resource version set), throwing if none is set.
12551264
* <li>{@link #matchAndFilter(Matcher)} / {@link #matchAndFilterWithDefaultMatcher(UpdateType)}
12561265
* - skip the write when the desired state already matches the actual state, otherwise write
12571266
* and filter the own event.
@@ -1265,8 +1274,8 @@ public static class Options {
12651274

12661275
private static final Options ALWAYS_FILTER = new Options(Mode.FORCE_FILTER, null);
12671276
private static final Options ONLY_CACHE = new Options(Mode.CACHE_ONLY, null);
1268-
private static final Options FILTER_IF_OPTIMISTIC_LOCKING =
1269-
new Options(Mode.FILTER_IF_OPTIMISTIC_LOCKING, null);
1277+
private static final Options FILTER_WITH_OPTIMISTIC_LOCKING =
1278+
new Options(Mode.FILTER_WITH_OPTIMISTIC_LOCKING, null);
12701279

12711280
private final Mode mode;
12721281
private final Matcher matcher;
@@ -1310,14 +1319,31 @@ public static Options cacheOnly(Matcher matcher) {
13101319
}
13111320

13121321
/**
1313-
* Filters the own event only when the write uses optimistic locking (a resource version is set
1314-
* on the written resource); otherwise only caches the response. This is the safe default for
1315-
* plain update/patch operations.
1322+
* Filters the own event resulting from the write, requiring the write to use optimistic locking
1323+
* (a resource version set on the written resource). If no resource version is set an {@link
1324+
* IllegalArgumentException} is thrown when the operation is performed. Requiring optimistic
1325+
* locking guarantees that a concurrent third-party change is rejected by the API server rather
1326+
* than being silently filtered out.
13161327
*
1317-
* @return options that filter the own event only when optimistic locking is used
1328+
* @return options that filter the own event, requiring optimistic locking
1329+
*/
1330+
public static Options filterWithOptimisticLocking() {
1331+
return FILTER_WITH_OPTIMISTIC_LOCKING;
1332+
}
1333+
1334+
/**
1335+
* Like {@link #filterWithOptimisticLocking()} but additionally skips the write when the desired
1336+
* state already matches the actual (cached) state according to the given {@link Matcher}. When
1337+
* a write is needed it still requires optimistic locking (see {@link
1338+
* #filterWithOptimisticLocking()}).
1339+
*
1340+
* @param matcher the matcher used to decide whether the actual state already matches the
1341+
* desired
1342+
* @return options that match using the given matcher and, when a write is needed, filter the
1343+
* own event requiring optimistic locking
13181344
*/
1319-
public static Options filterIfOptimisticLocking() {
1320-
return FILTER_IF_OPTIMISTIC_LOCKING;
1345+
public static Options filterWithOptimisticLocking(Matcher matcher) {
1346+
return new Options(Mode.FILTER_WITH_OPTIMISTIC_LOCKING, matcher);
13211347
}
13221348

13231349
/**
@@ -1362,14 +1388,14 @@ public boolean requiresMatcher() {
13621388
/**
13631389
* The strategy used to decide how the own event resulting from a write is handled. This is a
13641390
* low-level enum; users should not reference it directly but select the desired behavior through
1365-
* the {@link Options} factory methods (e.g. {@link Options#filterIfOptimisticLocking()}, {@link
1391+
* the {@link Options} factory methods (e.g. {@link Options#filterWithOptimisticLocking()}, {@link
13661392
* Options#matchAndFilter(Matcher)}, {@link Options#cacheOnly()}, {@link
13671393
* Options#forceFilterEvents()}), which is why each constant links to its corresponding factory.
13681394
*/
13691395
@Experimental(API_MIGHT_CHANGE)
13701396
enum Mode {
1371-
/** See {@link Options#filterIfOptimisticLocking()}. */
1372-
FILTER_IF_OPTIMISTIC_LOCKING,
1397+
/** See {@link Options#filterWithOptimisticLocking()}. */
1398+
FILTER_WITH_OPTIMISTIC_LOCKING,
13731399
/** See {@link Options#matchAndFilter(Matcher)}. */
13741400
FILTER_IF_NOT_MATCHING,
13751401
/** See {@link Options#cacheOnly()}. */

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,8 @@
2121

2222
import io.fabric8.kubernetes.api.model.HasMetadata;
2323
import io.javaoperatorsdk.operator.api.reconciler.Context;
24-
import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller;
2524
import io.javaoperatorsdk.operator.processing.ResourceIDMapper;
2625
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
27-
import io.javaoperatorsdk.operator.processing.event.ResourceID;
2826
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
2927
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
3028

@@ -96,11 +94,7 @@ private void handleExplicitStateDelete(P primary, R secondary, Context<P> contex
9694
@SuppressWarnings({"rawtypes", "unchecked", "unused"})
9795
protected void handleExplicitStateCreation(P primary, R created, Context<P> context) {
9896
var resource = dependentResourceWithExplicitState.stateResource(primary, created);
99-
var stateResource = context.getClient().resource(resource).create();
100-
if (externalStateEventSource != null) {
101-
((RecentOperationCacheFiller) externalStateEventSource)
102-
.handleRecentResourceCreate(ResourceID.fromResource(primary), stateResource);
103-
}
97+
context.resourceOperations().create(resource);
10498
}
10599

106100
@Override

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

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -408,25 +408,7 @@ void onlyCacheModeSkipsEventFiltering() {
408408
}
409409

410410
@Test
411-
void filterIfOptimisticLockingCachesWithoutFilteringWhenNoResourceVersion() {
412-
var desired = TestUtils.testCustomResource1();
413-
desired.getMetadata().setResourceVersion(null);
414-
var ies = mock(ManagedInformerEventSource.class);
415-
when(ies.updateAndCacheResource(any(), any(UnaryOperator.class))).thenReturn(desired);
416-
417-
resourceOperations.resourcePatch(
418-
desired,
419-
null,
420-
UnaryOperator.identity(),
421-
ies,
422-
ResourceOperations.Options.filterIfOptimisticLocking());
423-
424-
verify(ies, times(1)).updateAndCacheResource(eq(desired), any(UnaryOperator.class));
425-
verify(ies, never()).eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
426-
}
427-
428-
@Test
429-
void filterIfOptimisticLockingFiltersWhenResourceVersionPresent() {
411+
void filterWithOptimisticLockingFiltersWhenResourceVersionPresent() {
430412
var desired = TestUtils.testCustomResource1();
431413
desired.getMetadata().setResourceVersion("1");
432414
var ies = mock(ManagedInformerEventSource.class);
@@ -438,7 +420,7 @@ void filterIfOptimisticLockingFiltersWhenResourceVersionPresent() {
438420
null,
439421
UnaryOperator.identity(),
440422
ies,
441-
ResourceOperations.Options.filterIfOptimisticLocking());
423+
ResourceOperations.Options.filterWithOptimisticLocking());
442424

443425
verify(ies, times(1))
444426
.eventFilteringUpdateAndCacheResource(eq(desired), any(UnaryOperator.class));
@@ -532,32 +514,18 @@ void updateForceFilterFiltersAndCallsClientUpdate() {
532514
}
533515

534516
@Test
535-
void updateFilterIfOptimisticLockingFiltersWhenResourceVersionPresent() {
517+
void updateFilterWithOptimisticLockingFiltersWhenResourceVersionPresent() {
536518
var resource = TestUtils.testCustomResource1();
537519
resource.getMetadata().setResourceVersion("1");
538520
wireVerbMocks();
539521

540-
resourceOperations.update(resource, ResourceOperations.Options.filterIfOptimisticLocking());
522+
resourceOperations.update(resource, ResourceOperations.Options.filterWithOptimisticLocking());
541523

542524
verify(verbEventSource, times(1))
543525
.eventFilteringUpdateAndCacheResource(eq(resource), any(UnaryOperator.class));
544526
verify(verbEventSource, never()).updateAndCacheResource(any(), any(UnaryOperator.class));
545527
}
546528

547-
@Test
548-
void updateFilterIfOptimisticLockingCachesWhenNoResourceVersion() {
549-
var resource = TestUtils.testCustomResource1();
550-
resource.getMetadata().setResourceVersion(null);
551-
wireVerbMocks();
552-
553-
resourceOperations.update(resource, ResourceOperations.Options.filterIfOptimisticLocking());
554-
555-
verify(verbEventSource, times(1))
556-
.updateAndCacheResource(eq(resource), any(UnaryOperator.class));
557-
verify(verbEventSource, never())
558-
.eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class));
559-
}
560-
561529
@Test
562530
void updateWithMatcherMatchingSkipsWrite() {
563531
var desired = TestUtils.testCustomResource1();

operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/resourceoperations/SecondaryResourceOperationsReconciler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public UpdateControl<SecondaryResourceOperationsCustomResource> reconcile(
101101
}
102102
case UPDATE -> {
103103
actual.getData().put(DATA_KEY, APPLIED_VALUE);
104-
ops.update(actual, configMapEventSource, Options.filterIfOptimisticLocking());
104+
ops.update(actual, configMapEventSource, Options.filterWithOptimisticLocking());
105105
}
106106
case JSON_PATCH ->
107107
ops.jsonPatch(
@@ -111,10 +111,10 @@ public UpdateControl<SecondaryResourceOperationsCustomResource> reconcile(
111111
return cm;
112112
},
113113
configMapEventSource,
114-
Options.filterIfOptimisticLocking());
114+
Options.filterWithOptimisticLocking());
115115
case JSON_MERGE_PATCH -> {
116116
var desired = desiredConfigMap(resource, APPLIED_VALUE);
117-
ops.jsonMergePatch(desired, configMapEventSource, Options.filterIfOptimisticLocking());
117+
ops.jsonMergePatch(desired, configMapEventSource, Options.filterWithOptimisticLocking());
118118
}
119119
default -> throw new IllegalStateException("Unexpected operation: " + operation);
120120
}

0 commit comments

Comments
 (0)