3939import 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 */
49104public 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
0 commit comments