Skip to content
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
3fa2136
feat: all-event mode
csviri Aug 11, 2025
10c931b
wip
csviri Aug 11, 2025
f73fbc7
wip
csviri Aug 15, 2025
ae9ab80
wip
csviri Aug 15, 2025
27f009b
wip
csviri Aug 15, 2025
dda1311
wip
csviri Aug 21, 2025
7471d3b
wip
csviri Aug 21, 2025
9433cf8
wip
csviri Aug 21, 2025
02711c3
Working integration test
csviri Aug 21, 2025
cc10c4d
wip
csviri Aug 21, 2025
9b13134
wip
csviri Aug 21, 2025
6d1b374
wip
csviri Aug 21, 2025
0fb71d4
delete notes
csviri Aug 21, 2025
8ed16a7
fix
csviri Aug 21, 2025
6cdfd1d
fix
csviri Aug 21, 2025
9e1b28f
wip
csviri Aug 21, 2025
bdffb64
wip
csviri Sep 1, 2025
ad8c37c
wip
csviri Sep 1, 2025
962ea4f
Finalizer utils
csviri Sep 1, 2025
ff13791
tests
csviri Sep 2, 2025
ac01a98
test
csviri Sep 2, 2025
b62dfcb
wip
csviri Sep 2, 2025
3c9ed9d
wip
csviri Sep 2, 2025
b8d7dae
wip
csviri Sep 2, 2025
11e08c1
Changes to processAllEventInReconciler
csviri Sep 3, 2025
14255c6
naming
csviri Sep 4, 2025
e210e64
naming
csviri Sep 4, 2025
fa28ca3
wip
csviri Sep 4, 2025
98e8a8c
wip
csviri Sep 4, 2025
8b00785
wip
csviri Sep 4, 2025
1a633fc
wip
csviri Sep 4, 2025
7c4201c
fix
csviri Sep 5, 2025
ddbb1cb
wip
csviri Sep 5, 2025
b50c380
test fix
csviri Sep 5, 2025
8ae1e59
wip
csviri Sep 5, 2025
0b93bdc
wip
csviri Sep 5, 2025
5161479
wip
csviri Sep 16, 2025
8c10ddc
wip
csviri Sep 16, 2025
c2b91bc
wip
csviri Sep 16, 2025
adb3571
wip
csviri Sep 16, 2025
3d1895d
docs
csviri Sep 17, 2025
94c4c74
javadoc
csviri Sep 17, 2025
c5ff462
wip
csviri Sep 17, 2025
b890153
wip
csviri Sep 17, 2025
3930a37
wip
csviri Sep 17, 2025
cb1baf5
wip
csviri Sep 17, 2025
3800497
utils integration test
csviri Sep 17, 2025
18e6a40
test
csviri Sep 17, 2025
c4b5371
fixes for code review
csviri Sep 24, 2025
03410ae
docs: improve wording
metacosm Oct 2, 2025
c787ce3
Update operator-framework-core/src/main/java/io/javaoperatorsdk/opera…
csviri Oct 2, 2025
6a084c1
Update operator-framework-core/src/main/java/io/javaoperatorsdk/opera…
csviri Oct 2, 2025
7b050f0
format
csviri Oct 2, 2025
e8e6a3a
fix
csviri Oct 2, 2025
4b80885
docs
csviri Oct 2, 2025
3a840a7
comments on integration tests
csviri Oct 3, 2025
64c9212
improve and add unit test for event processor
csviri Oct 3, 2025
53d003e
Update operator-framework-core/src/main/java/io/javaoperatorsdk/opera…
csviri Oct 3, 2025
cdd6e42
missing javadoc
csviri Oct 3, 2025
f5fbfbc
javadoc improvements
csviri Oct 7, 2025
525413e
additional sample
csviri Oct 7, 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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: add @SInCE here and below?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added, thank you!


/**
* 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.
*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: add @SInCE?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added, thank you!

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