Skip to content
Merged
Show file tree
Hide file tree
Changes from 50 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
48 changes: 48 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,51 @@ 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 even for `Delete` 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, fall 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. This
mechanism, however, only works when all secondary resources are Kubernetes resources in the same namespace as the
primary resource.
2. The controller uses finalizers (the controller implements the `Cleaner` interface), when explicit cleanup logic is
required, 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 neither of those cases trigger the `reconcile` method of the controller on the `Delete` event of the primary
resource. When a finalizer is used, the SDK calls the `cleanup` method of the `Cleaner` implementation when the resource
is marked for deletion and the finalizer specified by the controller is present on the primary resource. When there is
no finalizer, there is no need to call the `reconcile` method on a `Delete` event since all the cleanup will be done by
the garbage collector. This avoids 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, the `reconcile` method will be triggered on ALL events (so also `Delete` events), making it
possible to support the above use cases.

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 is a successful reconciliation after a `Delete` event was received for the primary
resource
and reconciliation is not re-scheduled.
- you cannot use the `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