Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
7e368db
feat: all-event mode
csviri Aug 11, 2025
8ca44d2
wip
csviri Aug 11, 2025
525fae7
wip
csviri Aug 15, 2025
9bcdc84
wip
csviri Aug 15, 2025
b1eb9bf
wip
csviri Aug 15, 2025
cfbb8b5
wip
csviri Aug 21, 2025
c3ad61d
wip
csviri Aug 21, 2025
27850ba
wip
csviri Aug 21, 2025
dd87126
Working integration test
csviri Aug 21, 2025
5a5301e
wip
csviri Aug 21, 2025
00d45fa
wip
csviri Aug 21, 2025
cfaa9e8
wip
csviri Aug 21, 2025
a84fc0c
delete notes
csviri Aug 21, 2025
16cf673
fix
csviri Aug 21, 2025
6804b18
fix
csviri Aug 21, 2025
a11686c
wip
csviri Aug 21, 2025
2a51d4d
wip
csviri Sep 1, 2025
3b11123
wip
csviri Sep 1, 2025
69678ad
Finalizer utils
csviri Sep 1, 2025
37f9e3e
tests
csviri Sep 2, 2025
f1a4de6
test
csviri Sep 2, 2025
77db60d
wip
csviri Sep 2, 2025
0b4371e
wip
csviri Sep 2, 2025
816969f
wip
csviri Sep 2, 2025
43bfc6c
Changes to processAllEventInReconciler
csviri Sep 3, 2025
28751a3
naming
csviri Sep 4, 2025
495e357
naming
csviri Sep 4, 2025
6884b1c
wip
csviri Sep 4, 2025
e8d293b
wip
csviri Sep 4, 2025
0b805f7
wip
csviri Sep 4, 2025
bf09f63
wip
csviri Sep 4, 2025
923f9bd
fix
csviri Sep 5, 2025
f6fa483
wip
csviri Sep 5, 2025
66858dd
test fix
csviri Sep 5, 2025
167b3c0
wip
csviri Sep 5, 2025
eb78dcc
wip
csviri Sep 5, 2025
7808fb6
wip
csviri Sep 16, 2025
ac1a026
wip
csviri Sep 16, 2025
04938f4
wip
csviri Sep 16, 2025
c884fb7
wip
csviri Sep 16, 2025
92a9e72
docs
csviri Sep 17, 2025
f0c393b
javadoc
csviri Sep 17, 2025
1765745
wip
csviri Sep 17, 2025
557012f
wip
csviri Sep 17, 2025
d79ee39
wip
csviri Sep 17, 2025
5856e3e
wip
csviri Sep 17, 2025
54109e8
utils integration test
csviri Sep 17, 2025
c5c96b3
test
csviri Sep 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/content/en/docs/documentation/reconciler.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,44 @@ called, either by calling any of the `PrimeUpdateAndCacheUtils` methods again or
updated via `PrimaryUpdateAndCacheUtils`.

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).

### Trigger reconciliation on all events

TLDR; We provide an execution mode where `reconcile` method is called on every event from event source.

The framework optimizes execution for generic use cases, which in almost all cases falls into two categories:

1. The controller does not use finalizers; thus when the primary resource is deleted, all the managed secondary
resources are cleaned up using the Kubernetes garbage collection mechanism, a.k.a., using owner references.
2. When finalizers are used (using `Cleaner` interface), thus when controller requires some explicit cleanup logic, typically for external
resources and when secondary resources are in different namespace than the primary resources (owner references
cannot be used in this case).

Note that for example framework neither of those cases triggers the reconciler on the `Delete` event of the primary resource.
When finalizer is used, it calls `cleaner(..)` method when resource is marked for deletion and our (not other) finalizer
is present. When there is no finalizer, does not make sense to call the `reconciel(..)` method on a `Delete` event
since all the cleanup will be done by the garbage collector. This way we spare reconciliation cycles.

However, there are cases when controllers do not strictly follow those patterns, typically when:
- Only some of the primary resources use finalizers, e.g., for some of the primary resources you need
to create an external resource for others not.
- You maintain some additional in memory caches (so not all the caches are encapsulated by an `EventSource`)
and you don't want to use finalizers. For those cases, you typically want to clean up your caches when the primary
resource is deleted.

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)
to `true`, as a result, `reconcile` method will be triggered on ALL events (so also `Delete` events), and you
are free to optimize you reconciliation for the use cases above and possibly others.

In this mode:
- even if the primary resource is already deleted from the Informer's cache, we will still pass the last known state
as the parameter for the reconciler. You can check if the resource is deleted using `Context.isPrimaryResourceDeleted()`.
- The retry, rate limiting, re-schedule, filters mechanisms work normally. (The internal caches related to the resource
are cleaned up only when there was a successful reconiliation after `Delete` event received for the primary resource
and reconciliation was not re-scheduled.
- you cannot use `Cleaner` interface. The framework assumes you will explicitly manage the finalizers. To
add finalizer you can use [`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).
- you cannot use managed dependent resources since those manage the finalizers and other logic related to the normal
execution mode.


5 changes: 5 additions & 0 deletions operator-framework-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kube-api-test-client-inject</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,12 +304,15 @@ private <P extends HasMetadata> ResolvedControllerConfiguration<P> controllerCon
final var dependentFieldManager =
fieldManager.equals(CONTROLLER_NAME_AS_FIELD_MANAGER) ? name : fieldManager;

var triggerReconcilerOnAllEvent =
annotation != null && annotation.triggerReconcilerOnAllEvent();

InformerConfiguration<P> informerConfig =
InformerConfiguration.builder(resourceClass)
.initFromAnnotation(annotation != null ? annotation.informer() : null, context)
.buildForController();

return new ResolvedControllerConfiguration<P>(
return new ResolvedControllerConfiguration<>(
name,
generationAware,
associatedReconcilerClass,
Expand All @@ -323,7 +326,8 @@ private <P extends HasMetadata> ResolvedControllerConfiguration<P> controllerCon
null,
dependentFieldManager,
this,
informerConfig);
informerConfig,
triggerReconcilerOnAllEvent);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,8 @@ default String fieldManager() {
}

<C> C getConfigurationFor(DependentResourceSpec<?, P, C> spec);

default boolean triggerReconcilerOnAllEvent() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class ControllerConfigurationOverrider<R extends HasMetadata> {
private Duration reconciliationMaxInterval;
private Map<DependentResourceSpec, Object> configurations;
private final InformerConfiguration<R>.Builder config;
private boolean triggerReconcilerOnAllEvent;

private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
this.finalizer = original.getFinalizerName();
Expand All @@ -42,6 +43,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
this.rateLimiter = original.getRateLimiter();
this.name = original.getName();
this.fieldManager = original.fieldManager();
this.triggerReconcilerOnAllEvent = original.triggerReconcilerOnAllEvent();
}

public ControllerConfigurationOverrider<R> withFinalizer(String finalizer) {
Expand Down Expand Up @@ -154,6 +156,12 @@ public ControllerConfigurationOverrider<R> withFieldManager(String dependentFiel
return this;
}

public ControllerConfigurationOverrider<R> withTriggerReconcilerOnAllEvent(
boolean triggerReconcilerOnAllEvent) {
this.triggerReconcilerOnAllEvent = triggerReconcilerOnAllEvent;
return this;
}

/**
* Sets a max page size limit when starting the informer. This will result in pagination while
* populating the cache. This means that longer lists will take multiple requests to fetch. See
Expand Down Expand Up @@ -198,6 +206,7 @@ public ControllerConfiguration<R> build() {
fieldManager,
original.getConfigurationService(),
config.buildForController(),
triggerReconcilerOnAllEvent,
original.getWorkflowSpec().orElse(null));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class ResolvedControllerConfiguration<P extends HasMetadata>
private final Map<DependentResourceSpec, Object> configurations;
private final ConfigurationService configurationService;
private final String fieldManager;
private final boolean triggerReconcilerOnAllEvent;
private WorkflowSpec workflowSpec;

public ResolvedControllerConfiguration(ControllerConfiguration<P> other) {
Expand All @@ -44,6 +45,7 @@ public ResolvedControllerConfiguration(ControllerConfiguration<P> other) {
other.fieldManager(),
other.getConfigurationService(),
other.getInformerConfig(),
other.triggerReconcilerOnAllEvent(),
other.getWorkflowSpec().orElse(null));
}

Expand All @@ -59,6 +61,7 @@ public ResolvedControllerConfiguration(
String fieldManager,
ConfigurationService configurationService,
InformerConfiguration<P> informerConfig,
boolean triggerReconcilerOnAllEvent,
WorkflowSpec workflowSpec) {
this(
name,
Expand All @@ -71,7 +74,8 @@ public ResolvedControllerConfiguration(
configurations,
fieldManager,
configurationService,
informerConfig);
informerConfig,
triggerReconcilerOnAllEvent);
setWorkflowSpec(workflowSpec);
}

Expand All @@ -86,7 +90,8 @@ protected ResolvedControllerConfiguration(
Map<DependentResourceSpec, Object> configurations,
String fieldManager,
ConfigurationService configurationService,
InformerConfiguration<P> informerConfig) {
InformerConfiguration<P> informerConfig,
boolean triggerReconcilerOnAllEvent) {
this.informerConfig = informerConfig;
this.configurationService = configurationService;
this.name = ControllerConfiguration.ensureValidName(name, associatedReconcilerClassName);
Expand All @@ -99,6 +104,7 @@ protected ResolvedControllerConfiguration(
this.finalizer =
ControllerConfiguration.ensureValidFinalizerName(finalizer, getResourceTypeName());
this.fieldManager = fieldManager;
this.triggerReconcilerOnAllEvent = triggerReconcilerOnAllEvent;
}

protected ResolvedControllerConfiguration(
Expand All @@ -117,7 +123,8 @@ protected ResolvedControllerConfiguration(
null,
null,
configurationService,
InformerConfiguration.builder(resourceClass).buildForController());
InformerConfiguration.builder(resourceClass).buildForController(),
false);
}

@Override
Expand Down Expand Up @@ -207,4 +214,9 @@ public <C> C getConfigurationFor(DependentResourceSpec<?, P, C> spec) {
public String fieldManager() {
return fieldManager;
}

@Override
public boolean triggerReconcilerOnAllEvent() {
return triggerReconcilerOnAllEvent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,21 @@ default <R> Stream<R> getSecondaryResourcesAsStream(Class<R> expectedType) {
* @return {@code true} is another reconciliation is already scheduled, {@code false} otherwise
*/
boolean isNextReconciliationImminent();

/**
* To check if the primary resource is already deleted. This value can be true only if you turn on
* {@link
* io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration#triggerReconcilerOnAllEvent()}
*
* @return true Delete event received for primary resource
*/
boolean isPrimaryResourceDeleted();

/**
* Check this only if {@link #isPrimaryResourceDeleted()} is true.
*
* @return true if the primary resource is deleted, but the last known state is only available
* from the caches of the underlying Informer, not from Delete event.
*/
boolean isPrimaryResourceFinalStateUnknown();
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,11 @@ MaxReconciliationInterval maxReconciliationInterval() default
* @return the name used as field manager for SSA operations
*/
String fieldManager() default CONTROLLER_NAME_AS_FIELD_MANAGER;

/**
* By settings to true, reconcile method will be triggered on every event, thus even for Delete
* event. You cannot use {@link Cleaner} or managed dependent resources in that case. See
* documentation for further details.
*/
boolean triggerReconcilerOnAllEvent() default false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,21 @@ public class DefaultContext<P extends HasMetadata> implements Context<P> {
private final ControllerConfiguration<P> controllerConfiguration;
private final DefaultManagedWorkflowAndDependentResourceContext<P>
defaultManagedDependentResourceContext;

public DefaultContext(RetryInfo retryInfo, Controller<P> controller, P primaryResource) {
private final boolean primaryResourceDeleted;
private final boolean primaryResourceFinalStateUnknown;

public DefaultContext(
RetryInfo retryInfo,
Controller<P> controller,
P primaryResource,
boolean primaryResourceDeleted,
boolean primaryResourceFinalStateUnknown) {
this.retryInfo = retryInfo;
this.controller = controller;
this.primaryResource = primaryResource;
this.controllerConfiguration = controller.getConfiguration();
this.primaryResourceDeleted = primaryResourceDeleted;
this.primaryResourceFinalStateUnknown = primaryResourceFinalStateUnknown;
this.defaultManagedDependentResourceContext =
new DefaultManagedWorkflowAndDependentResourceContext<>(controller, primaryResource, this);
}
Expand Down Expand Up @@ -119,6 +128,16 @@ public boolean isNextReconciliationImminent() {
.isNextReconciliationImminent(ResourceID.fromResource(primaryResource));
}

@Override
public boolean isPrimaryResourceDeleted() {
return primaryResourceDeleted;
}

@Override
public boolean isPrimaryResourceFinalStateUnknown() {
return primaryResourceFinalStateUnknown;
}

public DefaultContext<P> setRetryInfo(RetryInfo retryInfo) {
this.retryInfo = retryInfo;
return this;
Expand Down
Loading