Skip to content

Commit 51db943

Browse files
committed
wip
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent 9a4578f commit 51db943

3 files changed

Lines changed: 213 additions & 20 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
title: Migrating from v5.4 to v5.5
3+
description: Migrating from v5.4 to v5.5
4+
---
5+
6+
## No breaking API changes
7+
8+
v5.5 does **not** contain breaking API changes: existing code compiles and continues to work without
9+
modification. There is, however, one **behavioral** change around own-event filtering that you should
10+
be aware of (see below).
11+
12+
## `UpdateControl` no longer filters own events by default
13+
14+
In previous versions, updating the primary resource (or its status) by returning an `UpdateControl`
15+
from the reconciler would filter out the event caused by that own update, so the write did not
16+
trigger an additional reconciliation. Unfortunately, in some edge cases this could lead an
17+
incorrect behavior, thus filtering out events which should be propagated.
18+
19+
More precisely in case where for example the spec part of the primary resource was updated, while
20+
controller patched the status, but that patch status was a no-op operation. Note that
21+
these no-op operations are causing issue, which should not be done in first place.
22+
From 5.5 we provide methods in `ResourceOperations` which allow only operations which are
23+
correct.
24+
25+
From v5.5, `UpdateControl` **no longer filters these own events by default**. Returning an
26+
`UpdateControl` still updates the resource and keeps the cache read-after-write consistent, but the
27+
resulting update event is now delivered like any other event, which may cause an additional
28+
reconciliation.
29+
30+
This is safe (reconciliations are expected to be idempotent), but if you relied on the previous
31+
filtering — for example to avoid an extra reconciliation after a status update — use
32+
[`ResourceOperations`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperations.java)
33+
directly, which does filter own events.
34+
35+
### Using `ResourceOperations` instead
36+
37+
`ResourceOperations` is available from the reconciliation `Context` via
38+
`context.resourceOperations()`. Instead of returning an `UpdateControl`, perform the update through
39+
it and return `UpdateControl.noUpdate()`:
40+
41+
```java
42+
// before (v5.4): the own status update event was filtered
43+
@Override
44+
public UpdateControl<MyResource> reconcile(MyResource resource, Context<MyResource> context) {
45+
resource.setStatus(new MyStatus().setReady(true));
46+
return UpdateControl.patchStatus(resource);
47+
}
48+
```
49+
50+
```java
51+
// after (v5.5): filter the own event explicitly via ResourceOperations
52+
@Override
53+
public UpdateControl<MyResource> reconcile(MyResource resource, Context<MyResource> context) {
54+
resource.setStatus(new MyStatus().setReady(true));
55+
context.resourceOperations().serverSideApplyPrimaryStatus(resource);
56+
return UpdateControl.noUpdate();
57+
}
58+
```
59+
60+
`ResourceOperations` covers every update/patch strategy (server-side apply, update, JSON Patch, JSON
61+
Merge Patch) for both the whole resource and the status subresource, as well as their primary
62+
variants. By default these operations match the desired state against the actual (cached) state
63+
before writing and filter the own event, so they only issue a request to the Kubernetes API server
64+
when something actually changed.
65+
66+
> **Note**: Safe own-event filtering requires either a matcher (used by default) or the update to be
67+
> done with optimistic locking. See the `ResourceOperations` and `ResourceOperations.Options`
68+
> documentation for the available modes, the correctness requirements, and the caveats of the
69+
> default matchers.

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

Lines changed: 140 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,67 @@
3939
import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion;
4040

4141
/**
42-
* Provides useful operations to manipulate resources (server-side apply, patch, etc.) in an
43-
* idiomatic way, in particular to make sure that the latest version of the resource is present in
44-
* the caches for the next reconciliation. In other words, it provides read-cache-after-write
45-
* consistency.
42+
* Provides various, useful operations to manipulate resources (server-side apply, patch, etc.) in
43+
* an idiomatic ways. Provides improved update/patch/create operations to make sure that the latest
44+
* version of the resource is present in the caches for the next reconciliation, and filter own
45+
* update events. You can still use kubernetes client directly, these methods are however useful to
46+
* achieve better efficiency.
4647
*
47-
* @param <P> the resource type on which this object operates
48+
* <p>Every update/patch/create method comes in two flavors: a default one, and one that takes an
49+
* {@link Options} argument to control how the resulting own event is handled. The behavior is
50+
* selected through {@link Mode}:
51+
*
52+
* <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.
57+
* <li>{@link Options#matchAndFilter(Matcher)} / {@link
58+
* Options#matchAndFilterWithDefaultMatcher(io.javaoperatorsdk.operator.api.reconciler.matcher.UpdateType)}
59+
* ({@link Mode#FILTER_IF_NOT_MATCHING}) - before writing, the desired state is compared to
60+
* the actual (cached) state using the provided {@link Matcher}; if they already match the
61+
* write is skipped, otherwise it is performed and the own event is filtered.
62+
* <li>{@link Options#cacheOnly()} ({@link Mode#CACHE_ONLY}) - the response is only put into the
63+
* cache (read-cache-after-write consistency) and no own-event filtering is done.
64+
* <li>{@link Options#forceFilterEvents()} ({@link Mode#FORCE_FILTER}) - the own event is always
65+
* filtered, regardless of optimistic locking. Use only when correctness is otherwise
66+
* guaranteed (see the note below). This is mostly for internal usage, like in {@link
67+
* io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource}
68+
* where we match the resource explicitly before update.
69+
* </ul>
70+
*
71+
* <p><strong>Correctness of event filtering.</strong> Filtering an own update event is only safe if
72+
* the framework can tell an own write apart from a concurrent third-party write. This requires
73+
* <em>either</em>:
74+
*
75+
* <ul>
76+
* <li>a {@link Matcher} to be provided (so a filtered event can be confirmed to match the desired
77+
* state we just wrote), <em>or</em>
78+
* <li>the update to be done using <em>optimistic locking</em> (a resource version set on the
79+
* written resource), so a conflicting concurrent change is rejected by the API server rather
80+
* than silently swallowed.
81+
* </ul>
82+
*
83+
* <p>If neither holds - for example {@link Options#forceFilterEvents()} on a write without
84+
* optimistic locking and without a matcher - a concurrent external update happening within the
85+
* filtering window may be filtered out and thus missed until the next resync. Prefer providing a
86+
* matcher or using optimistic locking whenever own-event filtering is desired.
87+
*
88+
* <p><strong>Default matchers.</strong> For the matching modes a default {@link Matcher} is
89+
* provided for every {@link io.javaoperatorsdk.operator.api.reconciler.matcher.UpdateType} (see
90+
* {@link
91+
* Options#matchAndFilterWithDefaultMatcher(io.javaoperatorsdk.operator.api.reconciler.matcher.UpdateType)}),
92+
* so matching works out of the box. Note however that these default matchers are heuristics and may
93+
* have issues in some edge cases, so a workflow relying on them should be tested against the
94+
* concrete resources it manages. When a default matcher does not fit, provide your own via {@link
95+
* Options#matchAndFilter(Matcher)}.
96+
*
97+
* <p>Despite that caveat, matching is generally the most efficient way to handle updates: it covers
98+
* full event filtering <em>and</em> only performs a write when the actual state actually differs
99+
* from the desired one, thus also reducing the number of requests made against the Kubernetes API
100+
* server.
101+
*
102+
* @param <P> the primary resource type on which this object operates
48103
*/
49104
public class ResourceOperations<P extends HasMetadata> {
50105

@@ -696,14 +751,12 @@ public <R extends HasMetadata> R resourcePatch(R resource, UnaryOperator<R> upda
696751
return resourcePatch(resource, updateOperation, Options.filterIfOptimisticLocking());
697752
}
698753

699-
@Experimental(API_MIGHT_CHANGE)
700754
@SuppressWarnings({"rawtypes", "unchecked"})
701755
private <R extends HasMetadata> R resourcePatch(
702756
R desired, UnaryOperator<R> updateOperation, Options options) {
703757
return resourcePatch(desired, null, updateOperation, options);
704758
}
705759

706-
@Experimental(API_MIGHT_CHANGE)
707760
@SuppressWarnings({"rawtypes", "unchecked"})
708761
private <R extends HasMetadata> R resourcePatch(
709762
R desired, R actual, UnaryOperator<R> updateOperation, Options options) {
@@ -731,12 +784,17 @@ private <R extends HasMetadata> R resourcePatch(
731784
}
732785
}
733786

787+
/**
788+
* This method is public to ensure backward compatibility, we will make it private in next major
789+
* release.
790+
*/
791+
@Deprecated(forRemoval = true)
734792
public <R extends HasMetadata> R resourcePatch(
735793
R desired, UnaryOperator<R> updateOperation, ManagedInformerEventSource<R, P, ?> ies) {
736794
return resourcePatch(desired, updateOperation, ies, Options.filterIfOptimisticLocking());
737795
}
738796

739-
public <R extends HasMetadata> R resourcePatch(
797+
private <R extends HasMetadata> R resourcePatch(
740798
R desired,
741799
UnaryOperator<R> updateOperation,
742800
ManagedInformerEventSource<R, P, ?> ies,
@@ -745,7 +803,8 @@ public <R extends HasMetadata> R resourcePatch(
745803
return resourcePatch(desired, null, updateOperation, ies, options);
746804
}
747805

748-
public <R extends HasMetadata> R resourcePatch(
806+
// visible for testing
807+
<R extends HasMetadata> R resourcePatch(
749808
R desiredResource,
750809
R actualResource,
751810
UnaryOperator<R> updateOperation,
@@ -758,7 +817,6 @@ public <R extends HasMetadata> R resourcePatch(
758817
if (actualResource == null) {
759818
actualResource = ies.get(ResourceID.fromResource(desiredResource)).orElse(null);
760819
}
761-
// todo describe might require optimistic locking
762820
if (actualResource != null) {
763821
matches = matcher.matches(desiredResource, actualResource, context);
764822
}
@@ -769,8 +827,8 @@ public <R extends HasMetadata> R resourcePatch(
769827
if (matches) {
770828
return actualResource;
771829
}
830+
// this is to cover special case for jsonPatch were we should use actual resource as base
772831
var targetBaseResource = desiredResource != null ? desiredResource : actualResource;
773-
774832
boolean optimisticLocking = targetBaseResource.getMetadata().getResourceVersion() != null;
775833

776834
if (options.getMode() == Mode.CACHE_ONLY
@@ -968,6 +1026,28 @@ public P addFinalizerWithSSA(String finalizerName) {
9681026
}
9691027
}
9701028

1029+
/**
1030+
* Controls how an update/patch/create operation of {@link ResourceOperations} handles the own
1031+
* event resulting from the write. This is the entry point users interact with to tune caching and
1032+
* event filtering; instances are created through the static factory methods rather than a
1033+
* constructor, and each maps to a {@link Mode}.
1034+
*
1035+
* <p>See the {@link ResourceOperations} class documentation for a full description of the
1036+
* available strategies, their correctness requirements (a {@link Matcher} or optimistic locking
1037+
* is needed for safe own-event filtering), and the trade-offs of the default matchers.
1038+
*
1039+
* <ul>
1040+
* <li>{@link #filterIfOptimisticLocking()} - filter the own event only when the write uses
1041+
* optimistic locking, otherwise only cache the response.
1042+
* <li>{@link #matchAndFilter(Matcher)} / {@link #matchAndFilterWithDefaultMatcher(UpdateType)}
1043+
* - skip the write when the desired state already matches the actual state, otherwise write
1044+
* and filter the own event.
1045+
* <li>{@link #cacheOnly()} / {@link #cacheOnly(Matcher)} - only cache the response, no
1046+
* own-event filtering.
1047+
* <li>{@link #forceFilterEvents()} - always filter the own event (mostly for internal usage).
1048+
* Mostly for internal usage, if we are sure that the resource is matched before.
1049+
* </ul>
1050+
*/
9711051
@Experimental(API_MIGHT_CHANGE)
9721052
public static class Options {
9731053

@@ -977,25 +1057,53 @@ public static class Options {
9771057
new Options(Mode.FILTER_IF_OPTIMISTIC_LOCKING, null);
9781058

9791059
private final Mode mode;
980-
private Matcher matcher;
1060+
private final Matcher matcher;
9811061

9821062
private Options(Mode mode, Matcher matcher) {
9831063
this.mode = mode;
9841064
this.matcher = matcher;
9851065
}
9861066

1067+
/**
1068+
* Always filters the own event resulting from the write, regardless of optimistic locking. Safe
1069+
* only when correctness is otherwise guaranteed (see {@link ResourceOperations}); mostly for
1070+
* internal usage.
1071+
*
1072+
* @return options that always filter the own event
1073+
*/
9871074
public static Options forceFilterEvents() {
9881075
return ALWAYS_FILTER;
9891076
}
9901077

1078+
/**
1079+
* Only caches the response of the write for read-cache-after-write consistency, without doing
1080+
* any own-event filtering.
1081+
*
1082+
* @return options that only cache the response
1083+
*/
9911084
public static Options cacheOnly() {
9921085
return ONLY_CACHE;
9931086
}
9941087

1088+
/**
1089+
* Like {@link #cacheOnly()} but additionally skips the write when the desired state already
1090+
* matches the actual (cached) state according to the given {@link Matcher}.
1091+
*
1092+
* @param matcher the matcher used to decide whether the actual state already matches the
1093+
* desired
1094+
* @return options that cache only, skipping the write when already matching
1095+
*/
9951096
public static Options cacheOnly(Matcher matcher) {
9961097
return new Options(Mode.CACHE_ONLY, matcher);
9971098
}
9981099

1100+
/**
1101+
* Filters the own event only when the write uses optimistic locking (a resource version is set
1102+
* on the written resource); otherwise only caches the response. This is the safe default for
1103+
* plain update/patch operations.
1104+
*
1105+
* @return options that filter the own event only when optimistic locking is used
1106+
*/
9991107
public static Options filterIfOptimisticLocking() {
10001108
return FILTER_IF_OPTIMISTIC_LOCKING;
10011109
}
@@ -1013,6 +1121,14 @@ public static Options matchAndFilter(Matcher matcher) {
10131121
return new Options(Mode.FILTER_IF_NOT_MATCHING, matcher);
10141122
}
10151123

1124+
/**
1125+
* Same as {@link #matchAndFilter(Matcher)} but uses the default {@link Matcher} registered for
1126+
* the given {@link UpdateType}. See the {@link ResourceOperations} class documentation for the
1127+
* caveats of the default matchers.
1128+
*
1129+
* @param updateType the update type whose default matcher should be used
1130+
* @return options that match using the default matcher and filter the own event
1131+
*/
10161132
public static Options matchAndFilterWithDefaultMatcher(UpdateType updateType) {
10171133
return new Options(Mode.FILTER_IF_NOT_MATCHING, updateType.getMatcher());
10181134
}
@@ -1030,13 +1146,22 @@ public boolean requiresMatcher() {
10301146
}
10311147
}
10321148

1149+
/**
1150+
* The strategy used to decide how the own event resulting from a write is handled. This is a
1151+
* low-level enum; users should not reference it directly but select the desired behavior through
1152+
* the {@link Options} factory methods (e.g. {@link Options#filterIfOptimisticLocking()}, {@link
1153+
* Options#matchAndFilter(Matcher)}, {@link Options#cacheOnly()}, {@link
1154+
* Options#forceFilterEvents()}), which is why each constant links to its corresponding factory.
1155+
*/
10331156
@Experimental(API_MIGHT_CHANGE)
1034-
/** */
1035-
public enum Mode {
1157+
enum Mode {
1158+
/** See {@link Options#filterIfOptimisticLocking()}. */
10361159
FILTER_IF_OPTIMISTIC_LOCKING,
1160+
/** See {@link Options#matchAndFilter(Matcher)}. */
10371161
FILTER_IF_NOT_MATCHING,
1162+
/** See {@link Options#cacheOnly()}. */
10381163
CACHE_ONLY,
1039-
/** */
1164+
/** See {@link Options#forceFilterEvents()}. */
10401165
FORCE_FILTER,
10411166
}
10421167

operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/readcacheafterwrite/specchangeduringstatuspatch/SpecChangeDuringStatusPatchReconciler.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@
2727

2828
/**
2929
* On the first reconciliation the reconciler patches its own status, but keeps the event filtering
30-
* window (opened by {@link
31-
* io.javaoperatorsdk.operator.api.reconciler.ResourceOperations#resourcePatch}) open until the test
32-
* signals that it has changed the spec on the cluster. This reproduces the race where a spec change
33-
* lands while the controller's own status patch is in flight: the spec change event must still
34-
* propagate as a fresh reconciliation, it must not be absorbed as if it were our own status update.
30+
* window open until the test signals that it has changed the spec on the cluster. This reproduces
31+
* the race where a spec change lands while the controller's own status patch is in flight: the spec
32+
* change event must still propagate as a fresh reconciliation, it must not be absorbed as if it
33+
* were our own status update.
3534
*/
3635
@ControllerConfiguration
3736
public class SpecChangeDuringStatusPatchReconciler

0 commit comments

Comments
 (0)