Skip to content

Commit 482c01b

Browse files
authored
feat: option to triggering reconciler on all events (#2894)
Signed-off-by: Attila Mészáros <[email protected]>
1 parent 4b533c7 commit 482c01b

File tree

39 files changed

+1760
-127
lines changed

39 files changed

+1760
-127
lines changed

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,52 @@ called, either by calling any of the `PrimeUpdateAndCacheUtils` methods again or
210210
updated via `PrimaryUpdateAndCacheUtils`.
211211

212212
See related integration test [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache).
213+
214+
### Trigger reconciliation for all events
215+
216+
TLDR; We provide an execution mode where `reconcile` method is called on every event from event source.
217+
218+
The framework optimizes execution for generic use cases, which, in almost all cases, fall into two categories:
219+
220+
1. The controller does not use finalizers; thus when the primary resource is deleted, all the managed secondary
221+
resources are cleaned up using the Kubernetes garbage collection mechanism, a.k.a., using owner references. This
222+
mechanism, however, only works when all secondary resources are Kubernetes resources in the same namespace as the
223+
primary resource.
224+
2. The controller uses finalizers (the controller implements the `Cleaner` interface), when explicit cleanup logic is
225+
required, typically for external resources and when secondary resources are in different namespace than the primary
226+
resources (owner references cannot be used in this case).
227+
228+
Note that neither of those cases trigger the `reconcile` method of the controller on the `Delete` event of the primary
229+
resource. When a finalizer is used, the SDK calls the `cleanup` method of the `Cleaner` implementation when the resource
230+
is marked for deletion and the finalizer specified by the controller is present on the primary resource. When there is
231+
no finalizer, there is no need to call the `reconcile` method on a `Delete` event since all the cleanup will be done by
232+
the garbage collector. This avoids reconciliation cycles.
233+
234+
However, there are cases when controllers do not strictly follow those patterns, typically when:
235+
236+
- Only some of the primary resources use finalizers, e.g., for some of the primary resources you need
237+
to create an external resource for others not.
238+
- You maintain some additional in memory caches (so not all the caches are encapsulated by an `EventSource`)
239+
and you don't want to use finalizers. For those cases, you typically want to clean up your caches when the primary
240+
resource is deleted.
241+
242+
For such use cases you can set [`triggerReconcilerOnAllEvent`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java#L81)
243+
to `true`, as a result, the `reconcile` method will be triggered on ALL events (so also `Delete` events), making it
244+
possible to support the above use cases.
245+
246+
In this mode:
247+
248+
- even if the primary resource is already deleted from the Informer's cache, we will still pass the last known state
249+
as the parameter for the reconciler. You can check if the resource is deleted using
250+
`Context.isPrimaryResourceDeleted()`.
251+
- The retry, rate limiting, re-schedule, filters mechanisms work normally. The internal caches related to the resource
252+
are cleaned up only when there is a successful reconciliation after a `Delete` event was received for the primary
253+
resource
254+
and reconciliation is not re-scheduled.
255+
- you cannot use the `Cleaner` interface. The framework assumes you will explicitly manage the finalizers. To
256+
add finalizer you can use [
257+
`PrimeUpdateAndCacheUtils`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java#L308).
258+
- you cannot use managed dependent resources since those manage the finalizers and other logic related to the normal
259+
execution mode.
260+
261+
See also [sample](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling) for selectively adding finalizers for resources;

operator-framework-core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@
7979
<artifactId>awaitility</artifactId>
8080
<scope>test</scope>
8181
</dependency>
82+
<dependency>
83+
<groupId>io.fabric8</groupId>
84+
<artifactId>kube-api-test-client-inject</artifactId>
85+
<scope>test</scope>
86+
</dependency>
8287
</dependencies>
8388

8489
<build>

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,15 @@ private <P extends HasMetadata> ResolvedControllerConfiguration<P> controllerCon
304304
final var dependentFieldManager =
305305
fieldManager.equals(CONTROLLER_NAME_AS_FIELD_MANAGER) ? name : fieldManager;
306306

307+
var triggerReconcilerOnAllEvent =
308+
annotation != null && annotation.triggerReconcilerOnAllEvent();
309+
307310
InformerConfiguration<P> informerConfig =
308311
InformerConfiguration.builder(resourceClass)
309312
.initFromAnnotation(annotation != null ? annotation.informer() : null, context)
310313
.buildForController();
311314

312-
return new ResolvedControllerConfiguration<P>(
315+
return new ResolvedControllerConfiguration<>(
313316
name,
314317
generationAware,
315318
associatedReconcilerClass,
@@ -323,7 +326,8 @@ private <P extends HasMetadata> ResolvedControllerConfiguration<P> controllerCon
323326
null,
324327
dependentFieldManager,
325328
this,
326-
informerConfig);
329+
informerConfig,
330+
triggerReconcilerOnAllEvent);
327331
}
328332

329333
/**

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,8 @@ default String fieldManager() {
9292
}
9393

9494
<C> C getConfigurationFor(DependentResourceSpec<?, P, C> spec);
95+
96+
default boolean triggerReconcilerOnAllEvent() {
97+
return false;
98+
}
9599
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class ControllerConfigurationOverrider<R extends HasMetadata> {
3030
private Duration reconciliationMaxInterval;
3131
private Map<DependentResourceSpec, Object> configurations;
3232
private final InformerConfiguration<R>.Builder config;
33+
private boolean triggerReconcilerOnAllEvent;
3334

3435
private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
3536
this.finalizer = original.getFinalizerName();
@@ -42,6 +43,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
4243
this.rateLimiter = original.getRateLimiter();
4344
this.name = original.getName();
4445
this.fieldManager = original.fieldManager();
46+
this.triggerReconcilerOnAllEvent = original.triggerReconcilerOnAllEvent();
4547
}
4648

4749
public ControllerConfigurationOverrider<R> withFinalizer(String finalizer) {
@@ -154,6 +156,12 @@ public ControllerConfigurationOverrider<R> withFieldManager(String dependentFiel
154156
return this;
155157
}
156158

159+
public ControllerConfigurationOverrider<R> withTriggerReconcilerOnAllEvent(
160+
boolean triggerReconcilerOnAllEvent) {
161+
this.triggerReconcilerOnAllEvent = triggerReconcilerOnAllEvent;
162+
return this;
163+
}
164+
157165
/**
158166
* Sets a max page size limit when starting the informer. This will result in pagination while
159167
* populating the cache. This means that longer lists will take multiple requests to fetch. See
@@ -198,6 +206,7 @@ public ControllerConfiguration<R> build() {
198206
fieldManager,
199207
original.getConfigurationService(),
200208
config.buildForController(),
209+
triggerReconcilerOnAllEvent,
201210
original.getWorkflowSpec().orElse(null));
202211
}
203212

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class ResolvedControllerConfiguration<P extends HasMetadata>
2929
private final Map<DependentResourceSpec, Object> configurations;
3030
private final ConfigurationService configurationService;
3131
private final String fieldManager;
32+
private final boolean triggerReconcilerOnAllEvent;
3233
private WorkflowSpec workflowSpec;
3334

3435
public ResolvedControllerConfiguration(ControllerConfiguration<P> other) {
@@ -44,6 +45,7 @@ public ResolvedControllerConfiguration(ControllerConfiguration<P> other) {
4445
other.fieldManager(),
4546
other.getConfigurationService(),
4647
other.getInformerConfig(),
48+
other.triggerReconcilerOnAllEvent(),
4749
other.getWorkflowSpec().orElse(null));
4850
}
4951

@@ -59,6 +61,7 @@ public ResolvedControllerConfiguration(
5961
String fieldManager,
6062
ConfigurationService configurationService,
6163
InformerConfiguration<P> informerConfig,
64+
boolean triggerReconcilerOnAllEvent,
6265
WorkflowSpec workflowSpec) {
6366
this(
6467
name,
@@ -71,7 +74,8 @@ public ResolvedControllerConfiguration(
7174
configurations,
7275
fieldManager,
7376
configurationService,
74-
informerConfig);
77+
informerConfig,
78+
triggerReconcilerOnAllEvent);
7579
setWorkflowSpec(workflowSpec);
7680
}
7781

@@ -86,7 +90,8 @@ protected ResolvedControllerConfiguration(
8690
Map<DependentResourceSpec, Object> configurations,
8791
String fieldManager,
8892
ConfigurationService configurationService,
89-
InformerConfiguration<P> informerConfig) {
93+
InformerConfiguration<P> informerConfig,
94+
boolean triggerReconcilerOnAllEvent) {
9095
this.informerConfig = informerConfig;
9196
this.configurationService = configurationService;
9297
this.name = ControllerConfiguration.ensureValidName(name, associatedReconcilerClassName);
@@ -99,6 +104,7 @@ protected ResolvedControllerConfiguration(
99104
this.finalizer =
100105
ControllerConfiguration.ensureValidFinalizerName(finalizer, getResourceTypeName());
101106
this.fieldManager = fieldManager;
107+
this.triggerReconcilerOnAllEvent = triggerReconcilerOnAllEvent;
102108
}
103109

104110
protected ResolvedControllerConfiguration(
@@ -117,7 +123,8 @@ protected ResolvedControllerConfiguration(
117123
null,
118124
null,
119125
configurationService,
120-
InformerConfiguration.builder(resourceClass).buildForController());
126+
InformerConfiguration.builder(resourceClass).buildForController(),
127+
false);
121128
}
122129

123130
@Override
@@ -207,4 +214,9 @@ public <C> C getConfigurationFor(DependentResourceSpec<?, P, C> spec) {
207214
public String fieldManager() {
208215
return fieldManager;
209216
}
217+
218+
@Override
219+
public boolean triggerReconcilerOnAllEvent() {
220+
return triggerReconcilerOnAllEvent;
221+
}
210222
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,23 @@ default <R> Stream<R> getSecondaryResourcesAsStream(Class<R> expectedType) {
7272
* @return {@code true} is another reconciliation is already scheduled, {@code false} otherwise
7373
*/
7474
boolean isNextReconciliationImminent();
75+
76+
/**
77+
* To check if the primary resource is already deleted. This value can be true only if you turn on
78+
* {@link
79+
* io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration#triggerReconcilerOnAllEvent()}
80+
*
81+
* @return true Delete event received for primary resource
82+
* @since 5.2.0
83+
*/
84+
boolean isPrimaryResourceDeleted();
85+
86+
/**
87+
* Check this only if {@link #isPrimaryResourceDeleted()} is true.
88+
*
89+
* @return true if the primary resource is deleted, but the last known state is only available
90+
* from the caches of the underlying Informer, not from Delete event.
91+
* @since 5.2.0
92+
*/
93+
boolean isPrimaryResourceFinalStateUnknown();
7594
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,11 @@ MaxReconciliationInterval maxReconciliationInterval() default
7777
* @return the name used as field manager for SSA operations
7878
*/
7979
String fieldManager() default CONTROLLER_NAME_AS_FIELD_MANAGER;
80+
81+
/**
82+
* By settings to true, reconcile method will be triggered on every event, thus even for Delete
83+
* event. You cannot use {@link Cleaner} or managed dependent resources in that case. See
84+
* documentation for further details.
85+
*/
86+
boolean triggerReconcilerOnAllEvent() default false;
8087
}

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,21 @@ public class DefaultContext<P extends HasMetadata> implements Context<P> {
2424
private final ControllerConfiguration<P> controllerConfiguration;
2525
private final DefaultManagedWorkflowAndDependentResourceContext<P>
2626
defaultManagedDependentResourceContext;
27-
28-
public DefaultContext(RetryInfo retryInfo, Controller<P> controller, P primaryResource) {
27+
private final boolean primaryResourceDeleted;
28+
private final boolean primaryResourceFinalStateUnknown;
29+
30+
public DefaultContext(
31+
RetryInfo retryInfo,
32+
Controller<P> controller,
33+
P primaryResource,
34+
boolean primaryResourceDeleted,
35+
boolean primaryResourceFinalStateUnknown) {
2936
this.retryInfo = retryInfo;
3037
this.controller = controller;
3138
this.primaryResource = primaryResource;
3239
this.controllerConfiguration = controller.getConfiguration();
40+
this.primaryResourceDeleted = primaryResourceDeleted;
41+
this.primaryResourceFinalStateUnknown = primaryResourceFinalStateUnknown;
3342
this.defaultManagedDependentResourceContext =
3443
new DefaultManagedWorkflowAndDependentResourceContext<>(controller, primaryResource, this);
3544
}
@@ -119,6 +128,16 @@ public boolean isNextReconciliationImminent() {
119128
.isNextReconciliationImminent(ResourceID.fromResource(primaryResource));
120129
}
121130

131+
@Override
132+
public boolean isPrimaryResourceDeleted() {
133+
return primaryResourceDeleted;
134+
}
135+
136+
@Override
137+
public boolean isPrimaryResourceFinalStateUnknown() {
138+
return primaryResourceFinalStateUnknown;
139+
}
140+
122141
public DefaultContext<P> setRetryInfo(RetryInfo retryInfo) {
123142
this.retryInfo = retryInfo;
124143
return this;

0 commit comments

Comments
 (0)