Documentation
JOSDK Documentation
This section contains detailed documentation for all Java Operator SDK features and concepts. Whether you’re building your first operator or need advanced configuration options, you’ll find comprehensive guides here.
Core Concepts
Advanced Features
Each guide includes practical examples and best practices to help you build robust, production-ready operators.
1 - Implementing a reconciler
How Reconciliation Works
The reconciliation process is event-driven and follows this flow:
Event Reception: Events trigger reconciliation from:
- Primary resources (usually custom resources) when created, updated, or deleted
- Secondary resources through registered event sources
Reconciliation Execution: Each reconciler handles a specific resource type and listens for events from the Kubernetes API server. When an event arrives, it triggers reconciliation unless one is already running for that resource. The framework ensures no concurrent reconciliation occurs for the same resource.
Post-Reconciliation Processing: After reconciliation completes, the framework:
- Schedules a retry if an exception was thrown
- Schedules new reconciliation if events were received during execution
- Schedules a timer event if rescheduling was requested (
UpdateControl.rescheduleAfter(..)
) - Finishes reconciliation if none of the above apply
The SDK core implements an event-driven system where events trigger reconciliation requests.
Implementing Reconciler and Cleaner Interfaces
To implement a reconciler, you must implement the Reconciler
interface.
A Kubernetes resource lifecycle has two phases depending on whether the resource is marked for deletion:
Normal Phase: The framework calls the reconcile
method for regular resource operations.
Deletion Phase: If the resource is marked for deletion and your Reconciler
implements the Cleaner
interface, only the cleanup
method is called. The framework automatically handles finalizers for you.
If you need explicit cleanup logic, always use finalizers. See Finalizer support for details.
Using UpdateControl
and DeleteControl
These classes control the behavior after reconciliation completes.
UpdateControl
can instruct the framework to:
- Update the status sub-resource
- Reschedule reconciliation with a time delay
@Override
public UpdateControl<MyCustomResource> reconcile(
EventSourceTestCustomResource resource, Context context) {
// omitted code
return UpdateControl.patchStatus(resource).rescheduleAfter(10, TimeUnit.SECONDS);
}
without an update:
@Override
public UpdateControl<MyCustomResource> reconcile(
EventSourceTestCustomResource resource, Context context) {
// omitted code
return UpdateControl.<MyCustomResource>noUpdate().rescheduleAfter(10, TimeUnit.SECONDS);
}
Note, though, that using EventSources
is the preferred way of scheduling since the
reconciliation is triggered only when a resource is changed, not on a timely basis.
At the end of the reconciliation, you typically update the status sub-resources.
It is also possible to update both the status and the resource with the patchResourceAndStatus
method. In this case,
the resource is updated first followed by the status, using two separate requests to the Kubernetes API.
From v5 UpdateControl
only supports patching the resources, by default
using Server Side Apply (SSA).
It is important to understand how SSA works in Kubernetes. Mainly, resources applied using SSA
should contain only the fields identifying the resource and those the user is interested in (a ‘fully specified intent’
in Kubernetes parlance), thus usually using a resource created from scratch, see
sample.
To contrast, see the same sample, this time without SSA.
Non-SSA based patch is still supported.
You can control whether or not to use SSA
using ConfigurationServcice.useSSAToPatchPrimaryResource()
and the related ConfigurationServiceOverrider.withUseSSAToPatchPrimaryResource
method.
Related integration test can be
found here.
Handling resources directly using the client, instead of delegating these updates operations to JOSDK by returning
an UpdateControl
at the end of your reconciliation, should work appropriately. However, we do recommend to
use UpdateControl
instead since JOSDK makes sure that the operations are handled properly, since there are subtleties
to be aware of. For example, if you are using a finalizer, JOSDK makes sure to include it in your fully specified intent
so that it is not unintentionally removed from the resource (which would happen if you omit it, since your controller is
the designated manager for that field and Kubernetes interprets the finalizer being gone from the specified intent as a
request for removal).
DeleteControl
typically instructs the framework to remove the finalizer after the dependent
resource are cleaned up in cleanup
implementation.
public DeleteControl cleanup(MyCustomResource customResource,Context context){
// omitted code
return DeleteControl.defaultDelete();
}
However, it is possible to instruct the SDK to not remove the finalizer, this allows to clean up
the resources in a more asynchronous way, mostly for cases when there is a long waiting period
after a delete operation is initiated. Note that in this case you might want to either schedule
a timed event to make sure cleanup
is executed again or use event sources to get notified
about the state changes of the deleted resource.
Finalizer Support
Kubernetes finalizers
make sure that your Reconciler
gets a chance to act before a resource is actually deleted
after it’s been marked for deletion. Without finalizers, the resource would be deleted directly
by the Kubernetes server.
Depending on your use case, you might or might not need to use finalizers. In particular, if
your operator doesn’t need to clean any state that would not be automatically managed by the
Kubernetes cluster (e.g. external resources), you might not need to use finalizers. You should
use the
Kubernetes garbage collection
mechanism as much as possible by setting owner references for your secondary resources so that
the cluster can automatically delete them for you whenever the associated primary resource is
deleted. Note that setting owner references is the responsibility of the Reconciler
implementation, though dependent resources
make that process easier.
If you do need to clean such a state, you need to use finalizers so that their
presence will prevent the Kubernetes server from deleting the resource before your operator is
ready to allow it. This allows for clean-up even if your operator was down when the resource was marked for deletion.
JOSDK makes cleaning resources in this fashion easier by taking care of managing finalizers
automatically for you when needed. The only thing you need to do is let the SDK know that your
operator is interested in cleaning the state associated with your primary resources by having it
implement
the Cleaner<P>
interface. If your Reconciler
doesn’t implement the Cleaner
interface, the SDK will consider
that you don’t need to perform any clean-up when resources are deleted and will, therefore, not activate finalizer support.
In other words, finalizer support is added only if your Reconciler
implements the Cleaner
interface.
The framework automatically adds finalizers as the first step, thus after a resource
is created but before the first reconciliation. The finalizer is added via a separate
Kubernetes API call. As a result of this update, the finalizer will then be present on the
resource. The reconciliation can then proceed as normal.
The automatically added finalizer will also be removed after the cleanup
is executed on
the reconciler. This behavior is customizable as explained
above when we addressed the use of
DeleteControl
.
You can specify the name of the finalizer to use for your Reconciler
using the
@ControllerConfiguration
annotation. If you do not specify a finalizer name, one will be automatically generated for you.
From v5, by default, the finalizer is added using Server Side Apply. See also UpdateControl
in docs.
Making sure the primary resource is up to date for the next reconciliation
It is typical to want to update the status subresource with the information that is available during the reconciliation.
This is sometimes referred to as the last observed state. When the primary resource is updated, though, the framework
does not cache the resource directly, relying instead on the propagation of the update to the underlying informer’s
cache. It can, therefore, happen that, if other events trigger other reconciliations, before the informer cache gets
updated, your reconciler does not see the latest version of the primary resource. While this might not typically be a
problem in most cases, as caches eventually become consistent, depending on your reconciliation logic, you might still
require the latest status version possible, for example, if the status subresource is used to store allocated values.
See Representing Allocated Values
from the Kubernetes docs for more details.
The framework provides the
PrimaryUpdateAndCacheUtils
utility class
to help with these use cases.
This class’ methods use internal caches in combination with update methods that leveraging
optimistic locking. If the update method fails on optimistic locking, it will retry
using a fresh resource from the server as base for modification.
@Override
public UpdateControl<StatusPatchCacheCustomResource> reconcile(
StatusPatchCacheCustomResource resource, Context<StatusPatchCacheCustomResource> context) {
// omitted logic
// update with SSA requires a fresh copy
var freshCopy = createFreshCopy(primary);
freshCopy.getStatus().setValue(statusWithState());
var updatedResource = PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResource(resource, freshCopy, context);
// the resource was updated transparently via the utils, no further action is required via UpdateControl in this case
return UpdateControl.noUpdate();
}
After the update PrimaryUpdateAndCacheUtils.ssaPatchStatusAndCacheResource
puts the result of the update into an internal
cache and the framework will make sure that the next reconciliation contains the most recent version of the resource.
Note that it is not necessarily the same version returned as response from the update, it can be a newer version since other parties
can do additional updates meanwhile. However, unless it has been explicitly modified, that
resource will contain the up-to-date status.
Note that you can also perform additional updates after the PrimaryUpdateAndCacheUtils.*PatchStatusAndCacheResource
is
called, either by calling any of the PrimeUpdateAndCacheUtils
methods again or via UpdateControl
. Using
PrimaryUpdateAndCacheUtils
guarantees that the next reconciliation will see a resource state no older than the version
updated via PrimaryUpdateAndCacheUtils
.
See related integration test here.
2 - Error handling and retries
How Automatic Retries Work
JOSDK automatically schedules retries whenever your Reconciler
throws an exception. This robust retry mechanism helps handle transient issues like network problems or temporary resource unavailability.
Default Retry Behavior
The default retry implementation covers most typical use cases with exponential backoff:
GenericRetry.defaultLimitedExponentialRetry()
.setInitialInterval(5000) // Start with 5-second delay
.setIntervalMultiplier(1.5D) // Increase delay by 1.5x each retry
.setMaxAttempts(5); // Maximum 5 attempts
Configuration Options
Using the @GradualRetry
annotation:
@ControllerConfiguration
@GradualRetry(maxAttempts = 3, initialInterval = 2000)
public class MyReconciler implements Reconciler<MyResource> {
// reconciler implementation
}
Custom retry implementation:
Specify a custom retry class in the @ControllerConfiguration
annotation:
@ControllerConfiguration(retry = MyCustomRetry.class)
public class MyReconciler implements Reconciler<MyResource> {
// reconciler implementation
}
Your custom retry class must:
- Provide a no-argument constructor for automatic instantiation
- Optionally implement
AnnotationConfigurable
for configuration from annotations. See GenericRetry
implementation for more details.
The Context object provides retry state information:
@Override
public UpdateControl<MyResource> reconcile(MyResource resource, Context<MyResource> context) {
if (context.isLastAttempt()) {
// Handle final retry attempt differently
resource.getStatus().setErrorMessage("Failed after all retry attempts");
return UpdateControl.patchStatus(resource);
}
// Normal reconciliation logic
// ...
}
Important Retry Behavior Notes
- Retry limits don’t block new events: When retry limits are reached, new reconciliations still occur for new events
- No retry on limit reached: If an error occurs after reaching the retry limit, no additional retries are scheduled until new events arrive
- Event-driven recovery: Fresh events can restart the retry cycle, allowing recovery from previously failed states
A successful execution resets the retry state.
Reconciler Error Handler
In order to facilitate error reporting you can override updateErrorStatus
method in Reconciler
:
public class MyReconciler implements Reconciler<WebPage> {
@Override
public ErrorStatusUpdateControl<WebPage> updateErrorStatus(
WebPage resource, Context<WebPage> context, Exception e) {
return handleError(resource, e);
}
}
The updateErrorStatus
method is called in case an exception is thrown from the Reconciler
. It is
also called even if no retry policy is configured, just after the reconciler execution.
RetryInfo.getAttemptCount()
is zero after the first reconciliation attempt, since it is not a
result of a retry (regardless of whether a retry policy is configured).
ErrorStatusUpdateControl
tells the SDK what to do and how to perform the status
update on the primary resource, which is always performed as a status sub-resource request. Note that
this update request will also produce an event and result in a reconciliation if the
controller is not generation-aware.
This feature is only available for the reconcile
method of the Reconciler
interface, since
there should not be updates to resources that have been marked for deletion.
Retry can be skipped in cases of unrecoverable errors:
ErrorStatusUpdateControl.patchStatus(customResource).withNoRetry();
Correctness and Automatic Retries
While it is possible to deactivate automatic retries, this is not desirable unless there is a particular reason.
Errors naturally occur, whether it be transient network errors or conflicts
when a given resource is handled by a Reconciler
but modified simultaneously by a user in
a different process. Automatic retries handle these cases nicely and will eventually result in a
successful reconciliation.
Retry, Rescheduling and Event Handling Common Behavior
Retry, reschedule, and standard event processing form a relatively complex system, each of these
functionalities interacting with the others. In the following, we describe the interplay of
these features:
A successful execution resets a retry and the rescheduled executions that were present before
the reconciliation. However, the reconciliation outcome can instruct a new rescheduling (UpdateControl
or DeleteControl
).
For example, if a reconciliation had previously been rescheduled for after some amount of time, but an event triggered
the reconciliation (or cleanup) in the meantime, the scheduled execution would be automatically cancelled, i.e.
rescheduling a reconciliation does not guarantee that one will occur precisely at that time; it simply guarantees that a reconciliation will occur at the latest.
Of course, it’s always possible to reschedule a new reconciliation at the end of that “automatic” reconciliation.
Similarly, if a retry was scheduled, any event from the cluster triggering a successful execution in the meantime
would cancel the scheduled retry (because there’s now no point in retrying something that already succeeded)
In case an exception is thrown, a retry is initiated. However, if an event is received
meanwhile, it will be reconciled instantly, and this execution won’t count as a retry attempt.
If the retry limit is reached (so no more automatic retry would happen), but a new event
received, the reconciliation will still happen, but won’t reset the retry, and will still be
marked as the last attempt in the retry info. The point (1) still holds - thus successful reconciliation will reset the retry - but no retry will happen in case of an error.
The thing to remember when it comes to retrying or rescheduling is that JOSDK tries to avoid unnecessary work. When
you reschedule an operation, you instruct JOSDK to perform that operation by the end of the rescheduling
delay at the latest. If something occurred on the cluster that triggers that particular operation (reconciliation or cleanup), then
JOSDK considers that there’s no point in attempting that operation again at the end of the specified delay since there
is no point in doing so anymore. The same idea also applies to retries.
3 - Event sources and related topics
See also
this blog post
.
Event sources are a relatively simple yet powerful and extensible concept to trigger controller
executions, usually based on changes to dependent resources. You typically need an event source
when you want your Reconciler
to be triggered when something occurs to secondary resources
that might affect the state of your primary resource. This is needed because a given
Reconciler
will only listen by default to events affecting the primary resource type it is
configured for. Event sources act as listen to events affecting these secondary resources so
that a reconciliation of the associated primary resource can be triggered when needed. Note that
these secondary resources need not be Kubernetes resources. Typically, when dealing with
non-Kubernetes objects or services, we can extend our operator to handle webhooks or websockets
or to react to any event coming from a service we interact with. This allows for very efficient
controller implementations because reconciliations are then only triggered when something occurs
on resources affecting our primary resources thus doing away with the need to periodically
reschedule reconciliations.

There are few interesting points here:
The CustomResourceEventSource
event source is a special one, responsible for handling events
pertaining to changes affecting our primary resources. This EventSource
is always registered
for every controller automatically by the SDK. It is important to note that events always relate
to a given primary resource. Concurrency is still handled for you, even in the presence of
EventSource
implementations, and the SDK still guarantees that there is no concurrent execution of
the controller for any given primary resource (though, of course, concurrent/parallel executions
of events pertaining to other primary resources still occur as expected).
Caching and Event Sources
Kubernetes resources are handled in a declarative manner. The same also holds true for event
sources. For example, if we define an event source to watch for changes of a Kubernetes Deployment
object using an InformerEventSource
, we always receive the whole associated object from the
Kubernetes API. This object might be needed at any point during our reconciliation process and
it’s best to retrieve it from the event source directly when possible instead of fetching it
from the Kubernetes API since the event source guarantees that it will provide the latest
version. Not only that, but many event source implementations also cache resources they handle
so that it’s possible to retrieve the latest version of resources without needing to make any
calls to the Kubernetes API, thus allowing for very efficient controller implementations.
Note after an operator starts, caches are already populated by the time the first reconciliation
is processed for the InformerEventSource
implementation. However, this does not necessarily
hold true for all event source implementations (PerResourceEventSource
for example). The SDK
provides methods to handle this situation elegantly, allowing you to check if an object is
cached, retrieving it from a provided supplier if not. See
related method
.
Registering Event Sources
To register event sources, your Reconciler
has to override the prepareEventSources
and return
list of event sources to register. One way to see this in action is
to look at the
WebPage example
(irrelevant details omitted):
import java.util.List;
@ControllerConfiguration
public class WebappReconciler
implements Reconciler<Webapp>, Cleaner<Webapp>, EventSourceInitializer<Webapp> {
// ommitted code
@Override
public List<EventSource<?, Webapp>> prepareEventSources(EventSourceContext<Webapp> context) {
InformerEventSourceConfiguration<Webapp> configuration =
InformerEventSourceConfiguration.from(Deployment.class, Webapp.class)
.withLabelSelector(SELECTOR)
.build();
return List.of(new InformerEventSource<>(configuration, context));
}
}
In the example above an InformerEventSource
is configured and registered.
InformerEventSource
is one of the bundled EventSource
implementations that JOSDK provides to
cover common use cases.
Managing Relation between Primary and Secondary Resources
Event sources let your operator know when a secondary resource has changed and that your
operator might need to reconcile this new information. However, in order to do so, the SDK needs
to somehow retrieve the primary resource associated with which ever secondary resource triggered
the event. In the Webapp
example above, when an event occurs on a tracked Deployment
, the
SDK needs to be able to identify which Webapp
resource is impacted by that change.
Seasoned Kubernetes users already know one way to track this parent-child kind of relationship:
using owner references. Indeed, that’s how the SDK deals with this situation by default as well,
that is, if your controller properly set owner references on your secondary resources, the SDK
will be able to follow that reference back to your primary resource automatically without you
having to worry about it.
However, owner references cannot always be used as they are restricted to operating within a
single namespace (i.e. you cannot have an owner reference to a resource in a different namespace)
and are, by essence, limited to Kubernetes resources so you’re out of luck if your secondary
resources live outside of a cluster.
This is why JOSDK provides the SecondaryToPrimaryMapper
interface so that you can provide
alternative ways for the SDK to identify which primary resource needs to be reconciled when
something occurs to your secondary resources. We even provide some of these alternatives in the
Mappers
class.
Note that, while a set of ResourceID
is returned, this set usually consists only of one
element. It is however possible to return multiple values or even no value at all to cover some
rare corner cases. Returning an empty set means that the mapper considered the secondary
resource event as irrelevant and the SDK will thus not trigger a reconciliation of the primary
resource in that situation.
Adding a SecondaryToPrimaryMapper
is typically sufficient when there is a one-to-many relationship
between primary and secondary resources. The secondary resources can be mapped to its primary
owner, and this is enough information to also get these secondary resources from the Context
object that’s passed to your Reconciler
.
There are however cases when this isn’t sufficient and you need to provide an explicit mapping
between a primary resource and its associated secondary resources using an implementation of the
PrimaryToSecondaryMapper
interface. This is typically needed when there are many-to-one or
many-to-many relationships between primary and secondary resources, e.g. when the primary resource
is referencing secondary resources.
See PrimaryToSecondaryIT
integration test for a sample.
Built-in EventSources
There are multiple event-sources provided out of the box, the following are some more central ones:
InformerEventSource
is probably the most important EventSource
implementation to know about. When you create an
InformerEventSource
, JOSDK will automatically create and register a SharedIndexInformer
, a
fabric8 Kubernetes client class, that will listen for events associated with the resource type
you configured your InformerEventSource
with. If you want to listen to Kubernetes resource
events, InformerEventSource
is probably the only thing you need to use. It’s highly
configurable so you can tune it to your needs. Take a look at
InformerEventSourceConfiguration
and associated classes for more details but some interesting features we can mention here is the
ability to filter events so that you can only get notified for events you care about. A
particularly interesting feature of the InformerEventSource
, as opposed to using your own
informer-based listening mechanism is that caches are particularly well optimized preventing
reconciliations from being triggered when not needed and allowing efficient operators to be written.
PerResourcePollingEventSource
PerResourcePollingEventSource
is used to poll external APIs, which don’t support webhooks or other event notifications. It
extends the abstract
ExternalResourceCachingEventSource
to support caching.
See MySQL Schema sample
for usage.
PollingEventSource
PollingEventSource
is similar to PerResourceCachingEventSource
except that, contrary to that event source, it
doesn’t poll a specific API separately per resource, but periodically and independently of
actually observed primary resources.
Inbound event sources
SimpleInboundEventSource
and
CachingInboundEventSource
are used to handle incoming events from webhooks and messaging systems.
ControllerResourceEventSource
ControllerResourceEventSource
is a special EventSource
implementation that you will never have to deal with directly. It is,
however, at the core of the SDK is automatically added for you: this is the main event source
that listens for changes to your primary resources and triggers your Reconciler
when needed.
It features smart caching and is really optimized to minimize Kubernetes API accesses and avoid
triggering unduly your Reconciler
.
More on the philosophy of the non Kubernetes API related event source see in
issue #729.
It is possible to handle resources for remote cluster with InformerEventSource
. To do so,
simply set a client that connects to a remote cluster:
InformerEventSourceConfiguration<WebPage> configuration =
InformerEventSourceConfiguration.from(SecondaryResource.class, PrimaryResource.class)
.withKubernetesClient(remoteClusterClient)
.withSecondaryToPrimaryMapper(Mappers.fromDefaultAnnotations());
You will also need to specify a SecondaryToPrimaryMapper
, since the default one
is based on owner references and won’t work across cluster instances. You could, for example, use the provided implementation that relies on annotations added to the secondary resources to identify the associated primary resource.
See related integration test.
Generation Awareness and Event Filtering
A best practice when an operator starts up is to reconcile all the associated resources because
changes might have occurred to the resources while the operator was not running.
When this first reconciliation is done successfully, the next reconciliation is triggered if either
dependent resources are changed or the primary resource .spec
field is changed. If other fields
like .metadata
are changed on the primary resource, the reconciliation could be skipped. This
behavior is supported out of the box and reconciliation is by default not triggered if
changes to the primary resource do not increase the .metadata.generation
field.
Note that changes to .metada.generation
are automatically handled by Kubernetes.
To turn off this feature, set generationAwareEventProcessing
to false
for the Reconciler
.
Max Interval Between Reconciliations
When informers / event sources are properly set up, and the Reconciler
implementation is
correct, no additional reconciliation triggers should be needed. However, it’s
a common practice
to have a failsafe periodic trigger in place, just to make sure resources are nevertheless
reconciled after a certain amount of time. This functionality is in place by default, with a
rather high time interval (currently 10 hours) after which a reconciliation will be
automatically triggered even in the absence of other events. See how to override this using the
standard annotation:
@ControllerConfiguration(maxReconciliationInterval = @MaxReconciliationInterval(
interval = 50,
timeUnit = TimeUnit.MILLISECONDS))
public class MyReconciler implements Reconciler<HasMetadata> {}
The event is not propagated at a fixed rate, rather it’s scheduled after each reconciliation. So the
next reconciliation will occur at most within the specified interval after the last reconciliation.
This feature can be turned off by setting maxReconciliationInterval
to Constants.NO_MAX_RECONCILIATION_INTERVAL
or any non-positive number.
The automatic retries are not affected by this feature so a reconciliation will be re-triggered
on error, according to the specified retry policy, regardless of this maximum interval setting.
Rate Limiting
It is possible to rate limit reconciliation on a per-resource basis. The rate limit also takes
precedence over retry/re-schedule configurations: for example, even if a retry was scheduled for
the next second but this request would make the resource go over its rate limit, the next
reconciliation will be postponed according to the rate limiting rules. Note that the
reconciliation is never cancelled, it will just be executed as early as possible based on rate
limitations.
Rate limiting is by default turned off, since correct configuration depends on the reconciler
implementation, in particular, on how long a typical reconciliation takes.
(The parallelism of reconciliation itself can be
limited ConfigurationService
by configuring the ExecutorService
appropriately.)
A default rate limiter implementation is provided, see:
PeriodRateLimiter
.
Users can override it by implementing their own
RateLimiter
and specifying this custom implementation using the rateLimiter
field of the
@ControllerConfiguration
annotation. Similarly to the Retry
implementations,
RateLimiter
implementations must provide an accessible, no-arg constructor for instantiation
purposes and can further be automatically configured from your own, provided annotation provided
your RateLimiter
implementation also implements the AnnotationConfigurable
interface,
parameterized by your custom annotation type.
To configure the default rate limiter use the @RateLimited
annotation on your
Reconciler
class. The following configuration limits each resource to reconcile at most twice
within a 3 second interval:
@RateLimited(maxReconciliations = 2, within = 3, unit = TimeUnit.SECONDS)
@ControllerConfiguration
public class MyReconciler implements Reconciler<MyCR> {
}
Thus, if a given resource was reconciled twice in one second, no further reconciliation for this
resource will happen before two seconds have elapsed. Note that, since rate is limited on a
per-resource basis, other resources can still be reconciled at the same time, as long, of course,
that they stay within their own rate limits.
Optimizing Caches
One of the ideas around the operator pattern is that all the relevant resources are cached, thus reconciliation is
usually very fast (especially if no resources are updated in the process) since the operator is then mostly working with
in-memory state. However for large clusters, caching huge amount of primary and secondary resources might consume lots
of memory. JOSDK provides ways to mitigate this issue and optimize the memory usage of controllers. While these features
are working and tested, we need feedback from real production usage.
Limiting caches for informers - thus for Kubernetes resources - is supported by ensuring that resources are in the cache
for a limited time, via a cache eviction of least recently used resources. This means that when resources are created
and frequently reconciled, they stay “hot” in the cache. However, if, over time, a given resource “cools” down, i.e. it
becomes less and less used to the point that it might not be reconciled anymore, it will eventually get evicted from the
cache to free up memory. If such an evicted resource were to become reconciled again, the bounded cache implementation
would then fetch it from the API server and the “hot/cold” cycle would start anew.
Since all resources need to be reconciled when a controller start, it is not practical to set a maximal cache size as
it’s desirable that all resources be cached as soon as possible to make the initial reconciliation process on start as
fast and efficient as possible, avoiding undue load on the API server. It’s therefore more interesting to gradually
evict cold resources than try to limit cache sizes.
See usage of the related implementation using Caffeine cache in integration
tests
for primary resources.
See
also CaffeineBoundedItemStores
for more details.
4 - Configurations
The Java Operator SDK (JOSDK) provides abstractions that work great out of the box. However, we recognize that default behavior isn’t always suitable for every use case. Numerous configuration options help you tailor the framework to your specific needs.
Configuration options operate at several levels:
- Operator-level using
ConfigurationService
- Reconciler-level using
ControllerConfiguration
- DependentResource-level using the
DependentResourceConfigurator
interface - EventSource-level where some event sources (like
InformerEventSource
) need fine-tuning to identify which events trigger the associated reconciler
Operator-Level Configuration
Configuration that impacts the entire operator is performed via the ConfigurationService
class. ConfigurationService
is an abstract class with different implementations based on which framework flavor you use (e.g., Quarkus Operator SDK replaces the default implementation). Configurations initialize with sensible defaults but can be changed during initialization.
For example, to disable CRD validation on startup and configure leader election:
Operator operator = new Operator( override -> override
.checkingCRDAndValidateLocalModel(false)
.withLeaderElectionConfiguration(new LeaderElectionConfiguration("bar", "barNS")));
Reconciler-Level Configuration
While reconcilers are typically configured using the @ControllerConfiguration
annotation, you can also override configuration at runtime when registering the reconciler with the operator. You can either:
- Pass a completely new
ControllerConfiguration
instance - Override specific aspects using a
ControllerConfigurationOverrider
Consumer
(preferred)
Operator operator;
Reconciler reconciler;
...
operator.register(reconciler, configOverrider ->
configOverrider.withFinalizer("my-nifty-operator/finalizer").withLabelSelector("foo=bar"));
Dynamically Changing Target Namespaces
A controller can be configured to watch a specific set of namespaces in addition of the
namespace in which it is currently deployed or the whole cluster. The framework supports
dynamically changing the list of these namespaces while the operator is running.
When a reconciler is registered, an instance of
RegisteredController
is returned, providing access to the methods allowing users to change watched namespaces as the
operator is running.
A typical scenario would probably involve extracting the list of target namespaces from a
ConfigMap
or some other input but this part is out of the scope of the framework since this is
use-case specific. For example, reacting to changes to a ConfigMap
would probably involve
registering an associated Informer
and then calling the changeNamespaces
method on
RegisteredController
.
public static void main(String[] args) {
KubernetesClient client = new DefaultKubernetesClient();
Operator operator = new Operator(client);
RegisteredController registeredController = operator.register(new WebPageReconciler(client));
operator.installShutdownHook();
operator.start();
// call registeredController further while operator is running
}
If watched namespaces change for a controller, it might be desirable to propagate these changes to
InformerEventSources
associated with the controller. In order to express this,
InformerEventSource
implementations interested in following such changes need to be
configured appropriately so that the followControllerNamespaceChanges
method returns true
:
@ControllerConfiguration
public class MyReconciler implements Reconciler<TestCustomResource> {
@Override
public Map<String, EventSource> prepareEventSources(
EventSourceContext<ChangeNamespaceTestCustomResource> context) {
InformerEventSource<ConfigMap, TestCustomResource> configMapES =
new InformerEventSource<>(InformerEventSourceConfiguration.from(ConfigMap.class, TestCustomResource.class)
.withNamespacesInheritedFromController(context)
.build(), context);
return EventSourceUtils.nameEventSources(configMapES);
}
}
As seen in the above code snippet, the informer will have the initial namespaces inherited from
controller, but also will adjust the target namespaces if it changes for the controller.
See also
the integration test
for this feature.
DependentResource-level configuration
It is possible to define custom annotations to configure custom DependentResource
implementations. In order to provide
such a configuration mechanism for your own DependentResource
implementations, they must be annotated with the
@Configured
annotation. This annotation defines 3 fields that tie everything together:
by
, which specifies which annotation class will be used to configure your dependents,with
, which specifies the class holding the configuration object for your dependents andconverter
, which specifies the ConfigurationConverter
implementation in charge of converting the annotation
specified by the by
field into objects of the class specified by the with
field.
ConfigurationConverter
instances implement a single configFrom
method, which will receive, as expected, the
annotation instance annotating the dependent resource instance to be configured, but it can also extract information
from the DependentResourceSpec
instance associated with the DependentResource
class so that metadata from it can be
used in the configuration, as well as the parent ControllerConfiguration
, if needed. The role of
ConfigurationConverter
implementations is to extract the annotation information, augment it with metadata from the
DependentResourceSpec
and the configuration from the parent controller on which the dependent is defined, to finally
create the configuration object that the DependentResource
instances will use.
However, one last element is required to finish the configuration process: the target DependentResource
class must
implement the ConfiguredDependentResource
interface, parameterized with the annotation class defined by the
@Configured
annotation by
field. This interface is called by the framework to inject the configuration at the
appropriate time and retrieve the configuration, if it’s available.
For example, KubernetesDependentResource
, a core implementation that the framework provides, can be configured via the
@KubernetesDependent
annotation. This set up is configured as follows:
@Configured(
by = KubernetesDependent.class,
with = KubernetesDependentResourceConfig.class,
converter = KubernetesDependentConverter.class)
public abstract class KubernetesDependentResource<R extends HasMetadata, P extends HasMetadata>
extends AbstractEventSourceHolderDependentResource<R, P, InformerEventSource<R, P>>
implements ConfiguredDependentResource<KubernetesDependentResourceConfig<R>> {
// code omitted
}
The @Configured
annotation specifies that KubernetesDependentResource
instances can be configured by using the
@KubernetesDependent
annotation, which gets converted into a KubernetesDependentResourceConfig
object by a
KubernetesDependentConverter
. That configuration object is then injected by the framework in the
KubernetesDependentResource
instance, after it’s been created, because the class implements the
ConfiguredDependentResource
interface, properly parameterized.
For more information on how to use this feature, we recommend looking at how this mechanism is implemented for
KubernetesDependentResource
in the core framework, SchemaDependentResource
in the samples or CustomAnnotationDep
in the BaseConfigurationServiceTest
test class.
EventSource-level configuration
TODO
5 - Observability
Runtime Info
RuntimeInfo
is used mainly to check the actual health of event sources. Based on this information it is easy to implement custom
liveness probes.
stopOnInformerErrorDuringStartup
setting, where this flag usually needs to be set to false, in order to control the exact liveness properties.
See also an example implementation in the
WebPage sample
Contextual Info for Logging with MDC
Logging is enhanced with additional contextual information using
MDC. The following attributes are available in most
parts of reconciliation logic and during the execution of the controller:
MDC Key | Value added from primary resource |
---|
resource.apiVersion | .apiVersion |
resource.kind | .kind |
resource.name | .metadata.name |
resource.namespace | .metadata.namespace |
resource.resourceVersion | .metadata.resourceVersion |
resource.generation | .metadata.generation |
resource.uid | .metadata.uid |
For more information about MDC see this link.
Metrics
JOSDK provides built-in support for metrics reporting on what is happening with your reconcilers in the form of
the Metrics
interface which can be implemented to connect to your metrics provider of choice, JOSDK calling the
methods as it goes about reconciling resources. By default, a no-operation implementation is provided thus providing a
no-cost sane default. A micrometer-based implementation is also provided.
You can use a different implementation by overriding the default one provided by the default ConfigurationService
, as
follows:
Metrics metrics; // initialize your metrics implementation
Operator operator = new Operator(client, o -> o.withMetrics(metrics));
Micrometer implementation
The micrometer implementation is typically created using one of the provided factory methods which, depending on which
is used, will return either a ready to use instance or a builder allowing users to customized how the implementation
behaves, in particular when it comes to the granularity of collected metrics. It is, for example, possible to collect
metrics on a per-resource basis via tags that are associated with meters. This is the default, historical behavior but
this will change in a future version of JOSDK because this dramatically increases the cardinality of metrics, which
could lead to performance issues.
To create a MicrometerMetrics
implementation that behaves how it has historically behaved, you can just create an
instance via:
MeterRegistry registry; // initialize your registry implementation
Metrics metrics = new MicrometerMetrics(registry);
Note, however, that this constructor is deprecated and we encourage you to use the factory methods instead, which either
return a fully pre-configured instance or a builder object that will allow you to configure more easily how the instance
will behave. You can, for example, configure whether or not the implementation should collect metrics on a per-resource
basis, whether or not associated meters should be removed when a resource is deleted and how the clean-up is performed.
See the relevant classes documentation for more details.
For example, the following will create a MicrometerMetrics
instance configured to collect metrics on a per-resource
basis, deleting the associated meters after 5 seconds when a resource is deleted, using up to 2 threads to do so.
MicrometerMetrics.newPerResourceCollectingMicrometerMetricsBuilder(registry)
.withCleanUpDelayInSeconds(5)
.withCleaningThreadNumber(2)
.build();
Operator SDK metrics
The micrometer implementation records the following metrics:
Meter name | Type | Tag names | Description |
---|
operator.sdk.reconciliations.executions.<reconciler name> | gauge | group, version, kind | Number of executions of the named reconciler |
operator.sdk.reconciliations.queue.size.<reconciler name> | gauge | group, version, kind | How many resources are queued to get reconciled by named reconciler |
operator.sdk.<map name> .size | gauge map size | | Gauge tracking the size of a specified map (currently unused but could be used to monitor caches size) |
operator.sdk.events.received | counter | <resource metadata> , event, action | Number of received Kubernetes events |
operator.sdk.events.delete | counter | <resource metadata> | Number of received Kubernetes delete events |
operator.sdk.reconciliations.started | counter | <resource metadata> , reconciliations.retries.last, reconciliations.retries.number | Number of started reconciliations per resource type |
operator.sdk.reconciliations.failed | counter | <resource metadata> , exception | Number of failed reconciliations per resource type |
operator.sdk.reconciliations.success | counter | <resource metadata> | Number of successful reconciliations per resource type |
operator.sdk.controllers.execution.reconcile | timer | <resource metadata> , controller | Time taken for reconciliations per controller |
operator.sdk.controllers.execution.cleanup | timer | <resource metadata> , controller | Time taken for cleanups per controller |
operator.sdk.controllers.execution.reconcile.success | counter | controller, type | Number of successful reconciliations per controller |
operator.sdk.controllers.execution.reconcile.failure | counter | controller, exception | Number of failed reconciliations per controller |
operator.sdk.controllers.execution.cleanup.success | counter | controller, type | Number of successful cleanups per controller |
operator.sdk.controllers.execution.cleanup.failure | counter | controller, exception | Number of failed cleanups per controller |
As you can see all the recorded metrics start with the operator.sdk
prefix. <resource metadata>
, in the table above,
refers to resource-specific metadata and depends on the considered metric and how the implementation is configured and
could be summed up as follows: group?, version, kind, [name, namespace?], scope
where the tags in square
brackets ([]
) won’t be present when per-resource collection is disabled and tags followed by a question mark are
omitted if the associated value is empty. Of note, when in the context of controllers’ execution metrics, these tag
names are prefixed with resource.
. This prefix might be removed in a future version for greater consistency.
6 - Other Features
The Java Operator SDK (JOSDK) is a high-level framework and tooling suite for implementing Kubernetes operators. By default, features follow best practices in an opinionated way. However, configuration options and feature flags are available to fine-tune or disable these features.
Support for Well-Known Kubernetes Resources
Controllers can be registered for standard Kubernetes resources (not just custom resources), such as Ingress
, Deployment
, and others.
See the integration test for an example of reconciling deployments.
public class DeploymentReconciler
implements Reconciler<Deployment>, TestExecutionInfoProvider {
@Override
public UpdateControl<Deployment> reconcile(
Deployment resource, Context context) {
// omitted code
}
}
Leader Election
Operators are typically deployed with a single active instance. However, you can deploy multiple instances where only one (the “leader”) processes events. This is achieved through “leader election.”
While all instances run and start their event sources to populate caches, only the leader processes events. If the leader crashes, other instances are already warmed up and ready to take over when a new leader is elected.
See sample configuration in the E2E test.
Automatic CRD Generation
Note: This feature is provided by the Fabric8 Kubernetes Client, not JOSDK itself.
To automatically generate CRD manifests from your annotated Custom Resource classes, add this dependency to your project:
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>crd-generator-apt</artifactId>
<scope>provided</scope>
</dependency>
The CRD will be generated in target/classes/META-INF/fabric8
(or target/test-classes/META-INF/fabric8
for test scope) with the CRD name suffixed by the generated spec version.
For example, a CR using the java-operator-sdk.io
group with a mycrs
plural form will result in these files:
mycrs.java-operator-sdk.io-v1.yml
mycrs.java-operator-sdk.io-v1beta1.yml
Note for Quarkus users: If you’re using the quarkus-operator-sdk
extension, you don’t need to add any extra dependency for CRD generation - the extension handles this automatically.
7 - Dependent resources and workflows
Dependent resources and workflows are features sometimes referenced as higher
level abstractions. These two related concepts provides an abstraction
over reconciliation of a single resource (Dependent resource) and the
orchestration of such resources (Workflows).
7.1 - Dependent resources
Motivations and Goals
Most operators need to deal with secondary resources when trying to realize the desired state
described by the primary resource they are in charge of. For example, the Kubernetes-native
Deployment
controller needs to manage ReplicaSet
instances as part of a Deployment
’s
reconciliation process. In this instance, ReplicatSet
is considered a secondary resource for
the Deployment
controller.
Controllers that deal with secondary resources typically need to perform the following steps, for
each secondary resource:
flowchart TD
compute[Compute desired secondary resource based on primary state] --> A
A{Secondary resource exists?}
A -- Yes --> match
A -- No --> Create --> Done
match{Matches desired state?}
match -- Yes --> Done
match -- No --> Update --> Done
While these steps are not difficult in and of themselves, there are some subtleties that can lead to
bugs or sub-optimal code if not done right. As this process is pretty much similar for each
dependent resource, it makes sense for the SDK to offer some level of support to remove the
boilerplate code associated with encoding these repetitive actions. It should
be possible to handle common cases (such as dealing with Kubernetes-native secondary resources) in a
semi-declarative way with only a minimal amount of code, JOSDK taking care of wiring everything
accordingly.
Moreover, in order for your reconciler to get informed of events on these secondary resources, you
need to configure and create event sources and maintain them. JOSDK already makes it rather easy
to deal with these, but dependent resources makes it even simpler.
Finally, there are also opportunities for the SDK to transparently add features that are even
trickier to get right, such as immediate caching of updated or created resources (so that your
reconciler doesn’t need to wait for a cluster roundtrip to continue its work) and associated
event filtering (so that something your reconciler just changed doesn’t re-trigger a
reconciliation, for example).
Design
DependentResource
vs. AbstractDependentResource
The new
DependentResource
interface lies at the core of the design and strives to encapsulate the logic that is required
to reconcile the state of the associated secondary resource based on the state of the primary
one. For most cases, this logic will follow the flow expressed above and JOSDK provides a very
convenient implementation of this logic in the form of the
AbstractDependentResource
class. If your logic doesn’t fit this pattern, though, you can still provide your
own reconcile
method implementation. While the benefits of using dependent resources are less
obvious in that case, this allows you to separate the logic necessary to deal with each
secondary resource in its own class that can then be tested in isolation via unit tests. You can
also use the declarative support with your own implementations as we shall see later on.
AbstractDependentResource
is designed so that classes extending it specify which functionality
they support by implementing trait interfaces. This design has been selected to express the fact
that not all secondary resources are completely under the control of the primary reconciler:
some dependent resources are only ever created or updated for example and we needed a way to let
JOSDK know when that is the case. We therefore provide trait interfaces: Creator
,
Updater
and Deleter
to express that the DependentResource
implementation will provide custom
functionality to create, update and delete its associated secondary resources, respectively. If
these traits are not implemented then parts of the logic described above is never triggered: if
your implementation doesn’t implement Creator
, for example, AbstractDependentResource
will
never try to create the associated secondary resource, even if it doesn’t exist. It is even
possible to not implement any of these traits and therefore create read-only dependent resources
that will trigger your reconciler whenever a user interacts with them but that are never
modified by your reconciler itself - however note that read-only dependent resources rarely make
sense, as it is usually simpler to register an event source for the target resource.
All subclasses
of AbstractDependentResource
can also implement
the Matcher
interface to customize how the SDK decides whether or not the actual state of the dependent
matches the desired state. This makes it convenient to use these abstract base classes for your
implementation, only customizing the matching logic. Note that in many cases, there is no need
to customize that logic as the SDK already provides convenient default implementations in the
form
of DesiredEqualsMatcher
and
GenericKubernetesResourceMatcher
implementations, respectively. If you want to provide custom logic, you only need your
DependentResource
implementation to implement the Matcher
interface as below, which shows
how to customize the default matching logic for Kubernetes resources to also consider annotations
and labels, which are ignored by default:
public class MyDependentResource extends KubernetesDependentResource<MyDependent, MyPrimary>
implements Matcher<MyDependent, MyPrimary> {
// your implementation
public Result<MyDependent> match(MyDependent actualResource, MyPrimary primary,
Context<MyPrimary> context) {
return GenericKubernetesResourceMatcher.match(this, actualResource, primary, context, true);
}
}
Batteries included: convenient DependentResource implementations!
JOSDK also offers several other convenient implementations building on top of
AbstractDependentResource
that you can use as starting points for your own implementations.
One such implementation is the KubernetesDependentResource
class that makes it really easy to work
with Kubernetes-native resources. In this case, you usually only need to provide an implementation
for the desired
method to tell JOSDK what the desired state of your secondary resource should
be based on the specified primary resource state.
JOSDK takes care of everything else using default implementations that you can override in case you
need more precise control of what’s going on.
We also provide implementations that make it easy to cache
(AbstractExternalDependentResource
) or poll for changes in external resources
(PollingDependentResource
, PerResourcePollingDependentResource
). All the provided
implementations can be found in the io/javaoperatorsdk/operator/processing/dependent
package of
the operator-framework-core
module.
Sample Kubernetes Dependent Resource
A typical use case, when a Kubernetes resource is fully managed - Created, Read, Updated and
Deleted (or set to be garbage collected). The following example shows how to create a
Deployment
dependent resource:
@KubernetesDependent(informer = @Informer(labelSelector = SELECTOR))
class DeploymentDependentResource extends CRUDKubernetesDependentResource<Deployment, WebPage> {
@Override
protected Deployment desired(WebPage webPage, Context<WebPage> context) {
var deploymentName = deploymentName(webPage);
Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml");
deployment.getMetadata().setName(deploymentName);
deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace());
deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName);
deployment.getSpec().getTemplate().getMetadata().getLabels()
.put("app", deploymentName);
deployment.getSpec().getTemplate().getSpec().getVolumes().get(0)
.setConfigMap(new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build());
return deployment;
}
}
The only thing that you need to do is to extend the CRUDKubernetesDependentResource
and
specify the desired state for your secondary resources based on the state of the primary one. In
the example above, we’re handling the state of a Deployment
secondary resource associated with
a WebPage
custom (primary) resource.
The @KubernetesDependent
annotation can be used to further configure managed dependent
resource that are extending KubernetesDependentResource
.
See the full source
code here
.
Managed Dependent Resources
As mentioned previously, one goal of this implementation is to make it possible to declaratively
create and wire dependent resources. You can annotate your reconciler with @Dependent
annotations that specify which DependentResource
implementation it depends upon.
JOSDK will take the appropriate steps to wire everything together and call your
DependentResource
implementations reconcile
method before your primary resource is reconciled.
This makes sense in most use cases where the logic associated with the primary resource is
usually limited to status handling based on the state of the secondary resources and the
resources are not dependent on each other. As an alternative, you can also invoke reconciliation explicitly,
event for managed workflows.
See Workflows for more details on how the dependent
resources are reconciled.
This behavior and automated handling is referred to as “managed” because the DependentResource
instances are managed by JOSDK, an example of which can be seen below:
@Workflow(
dependents = {
@Dependent(type = ConfigMapDependentResource.class),
@Dependent(type = DeploymentDependentResource.class),
@Dependent(type = ServiceDependentResource.class),
@Dependent(
type = IngressDependentResource.class,
reconcilePrecondition = ExposedIngressCondition.class)
})
public class WebPageManagedDependentsReconciler
implements Reconciler<WebPage>, ErrorStatusHandler<WebPage> {
// omitted code
@Override
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context) {
final var name = context.getSecondaryResource(ConfigMap.class).orElseThrow()
.getMetadata().getName();
webPage.setStatus(createStatus(name));
return UpdateControl.patchStatus(webPage);
}
}
See the full source code of
sample here
.
Standalone Dependent Resources
It is also possible to wire dependent resources programmatically. In practice this means that the
developer is responsible for initializing and managing the dependent resources as well as calling
their reconcile
method. However, this makes it possible for developers to fully customize the
reconciliation process. Standalone dependent resources should be used in cases when the managed use
case does not fit. You can, of course, also use Workflows when managing
resources programmatically.
You can see a commented example of how to do
so here.
Creating/Updating Kubernetes Resources
From version 4.4 of the framework the resources are created and updated
using Server Side Apply
, thus the desired state is simply sent using this approach to update the actual resource.
Comparing desired and actual state (matching)
During the reconciliation of a dependent resource, the desired state is matched with the actual
state from the caches. The dependent resource only gets updated on the server if the actual,
observed state differs from the desired one. Comparing these two states is a complex problem
when dealing with Kubernetes resources because a strict equality check is usually not what is
wanted due to the fact that multiple fields might be automatically updated or added by
the platform (
by dynamic admission controllers
or validation webhooks, for example). Solving this problem in a generic way is therefore a tricky
proposition.
JOSDK provides such a generic matching implementation which is used by default:
SSABasedGenericKubernetesResourceMatcher
This implementation relies on the managed fields used by the Server Side Apply feature to
compare only the values of the fields that the controller manages. This ensures that only
semantically relevant fields are compared. See javadoc for further details.
JOSDK versions prior to 4.4 were using a different matching algorithm as implemented in
GenericKubernetesResourceMatcher.
Since SSA is a complex feature, JOSDK implements a feature flag allowing users to switch between
these implementations. See
in ConfigurationService.
It is, however, important to note that these implementations are default, generic
implementations that the framework can provide expected behavior out of the box. In many
situations, these will work just fine but it is also possible to provide matching algorithms
optimized for specific use cases. This is easily done by simply overriding
the match(...)
method.
It is also possible to bypass the matching logic altogether to simply rely on the server-side
apply mechanism if always sending potentially unchanged resources to the cluster is not an issue.
JOSDK’s matching mechanism allows to spare some potentially useless calls to the Kubernetes API
server. To bypass the matching feature completely, simply override the match
method to always
return false
, thus telling JOSDK that the actual state never matches the desired one, making
it always update the resources using SSA.
WARNING: Older versions of Kubernetes before 1.25 would create an additional resource version for every SSA update
performed with certain resources - even though there were no actual changes in the stored resource - leading to infinite
reconciliations. This behavior was seen with Secrets using stringData
, Ingresses using empty string fields, and
StatefulSets using volume claim templates. The operator framework has added built-in handling for the StatefulSet issue.
If you encounter this issue on an older Kubernetes version, consider changing your desired state, turning off SSA for
that resource, or even upgrading your Kubernetes version. If you encounter it on a newer Kubernetes version, please log
an issue with the JOSDK and with upstream Kubernetes.
Telling JOSDK how to find which secondary resources are associated with a given primary resource
KubernetesDependentResource
automatically maps secondary resource to a primary by owner reference. This behavior can be
customized by implementing
SecondaryToPrimaryMapper
by the dependent resource.
See sample in one of the integration
tests here
.
Multiple Dependent Resources of Same Type
When dealing with multiple dependent resources of same type, the dependent resource implementation
needs to know which specific resource should be targeted when reconciling a given dependent
resource, since there could be multiple instances of that type which could possibly be used, each
associated with the same primary resource. In this situation, JOSDK automatically selects the appropriate secondary
resource matching the desired state associated with the primary resource. This makes sense because the desired
state computation already needs to be able to discriminate among multiple related secondary resources to tell JOSDK how
they should be reconciled.
There might be cases, though, where it might be problematic to call the desired
method several times (for example, because it is costly to do so),
it is always possible to override this automated discrimination using several means (consider in this priority order):
- Override the
targetSecondaryResourceID
method, if your DependentResource
extends KubernetesDependentResource
,
where it’s very often possible to easily determine the ResourceID
of the secondary resource. This would probably be
the easiest solution if you’re working with Kubernetes resources. - Override the
selectTargetSecondaryResource
method, if your DependentResource
extends AbstractDependentResource
.
This should be relatively simple to override this method to optimize the matching to your needs. You can see an
example of such an implementation in
the ExternalWithStateDependentResource
class. - As last resort, you can implement your own
getSecondaryResource
method on your DependentResource
implementation from scratch.
Sharing an Event Source Between Dependent Resources
Dependent resources usually also provide event sources. When dealing with multiple dependents of
the same type, one needs to decide whether these dependent resources should track the same
resources and therefore share a common event source, or, to the contrary, track completely
separate resources, in which case using separate event sources is advised.
Dependents can therefore reuse existing, named event sources by referring to their name. In the
declarative case, assuming a configMapSource
EventSource
has already been declared, this
would look as follows:
@Dependent(type = MultipleManagedDependentResourceConfigMap1.class,
useEventSourceWithName = "configMapSource")
A sample is provided as an integration test both:
for managed
For standalone
cases.
Bulk Dependent Resources
So far, all the cases we’ve considered were dealing with situations where the number of
dependent resources needed to reconcile the state expressed by the primary resource is known
when writing the code for the operator. There are, however, cases where the number of dependent
resources to be created depends on information found in the primary resource.
These cases are covered by the “bulk” dependent resources feature. To create such dependent
resources, your implementation should extend AbstractDependentResource
(at least indirectly) and
implement the
BulkDependentResource
interface.
Various examples are provided
as integration tests
.
To see how bulk dependent resources interact with workflow conditions, please refer to this
integration test.
External State Tracking Dependent Resources
It is sometimes necessary for a controller to track external (i.e. non-Kubernetes) state to
properly manage some dependent resources. For example, your controller might need to track the
state of a REST API resource, which, after being created, would be refer to by its identifier.
Such identifier would need to be tracked by your controller to properly retrieve the state of
the associated resource and/or assess if such a resource exists. While there are several ways to
support this use case, we recommend storing such information in a dedicated Kubernetes resources
(usually a ConfigMap
or a Secret
), so that it can be manipulated with common Kubernetes
mechanisms.
This particular use case is supported by the
AbstractExternalDependentResource
class that you can extend to suit your needs, as well as implement the
DependentResourceWithExplicitState
interface. Note that most of the JOSDK-provided dependent resource implementations such as
PollingDependentResource
or PerResourcePollingDependentResource
already extends
AbstractExternalDependentResource
, thus supporting external state tracking out of the box.
See integration test
as a sample.
For a better understanding it might be worth to study
a sample implementation
without dependent resources.
Please also refer to the docs for managing state in
general.
Combining Bulk and External State Tracking Dependent Resources
Both bulk and external state tracking features can be combined. In that
case, a separate, state-tracking resource will be created for each bulk dependent resource
created. For example, if three bulk dependent resources associated with external state are created,
three associated ConfigMaps
(assuming ConfigMaps
are used as a state-tracking resource) will
also be created, one per dependent resource.
See integration test
as a sample.
GenericKubernetesResource based Dependent Resources
In rare circumstances resource handling where there is no class representation or just typeless handling might be
needed.
Fabric8 Client
provides GenericKubernetesResource
to support that.
For dependent resource this is supported
by GenericKubernetesDependentResource
. See
samples here.
Other Dependent Resource Features
When a Kubernetes resource is created or updated the related informer (more precisely
the InformerEventSource
), eventually will receive an event and will cache the up-to-date
resource. Typically, though, there might be a small time window when calling the
getResource()
of the dependent resource or getting the resource from the EventSource
itself won’t return the just updated resource, in the case where the associated event hasn’t
been received from the Kubernetes API. The KubernetesDependentResource
implementation,
however, addresses this issue, so you don’t have to worry about it by making sure that it or
the related InformerEventSource
always return the up-to-date resource.
Another feature of KubernetesDependentResource
is to make sure that if a resource is created or
updated during the reconciliation, this particular change, which normally would trigger the
reconciliation again (since the resource has changed on the server), will, in fact, not
trigger the reconciliation again since we already know the state is as expected. This is a small
optimization. For example if during a reconciliation a ConfigMap
is updated using dependent
resources, this won’t trigger a new reconciliation. Such a reconciliation is indeed not
needed since the change originated from our reconciler. For this system to work properly,
though, it is required that changes are received only by one event source (this is a best
practice in general) - so for example if there are two config map dependents, either
there should be a shared event source between them, or a label selector on the event sources
to select only the relevant events, see
in related integration test
.
“Read-only” Dependent Resources vs. Event Source
See Integration test for a read-only
dependent here.
Some secondary resources only exist as input for the reconciliation process and are never
updated by a controller (they might, and actually usually do, get updated by users interacting
with the resources directly, however). This might be the case, for example, of a ConfigMap
that is
used to configure common characteristics of multiple resources in one convenient place.
In such situations, one might wonder whether it makes sense to create a dependent resource in
this case or simply use an EventSource
so that the primary resource gets reconciled whenever a
user changes the resource. Typical dependent resources provide a desired state that the
reconciliation process attempts to match. In the case of so-called read-only dependents, though,
there is no such desired state because the operator / controller will never update the resource
itself, just react to external changes to it. An EventSource
would achieve the same result.
Using a dependent resource for that purpose instead of a simple EventSource
, however, provides
several benefits:
- dependents can be created declaratively, while an event source would need to be manually created
- if dependents are already used in a controller, it makes sense to unify the handling of all
secondary resources as dependents from a code organization perspective
- dependent resources can also interact with the workflow feature, thus allowing the read-only
resource to participate in conditions, in particular to decide whether the primary
resource needs/can be reconciled using reconcile pre-conditions, block the progression of the workflow altogether with
ready post-conditions or have other dependents depend on them, in essence, read-only dependents can participate in
workflows just as any other dependents.
7.2 - Workflows
Overview
Kubernetes (k8s) does not have the notion of a resource “depending on” on another k8s resource,
at least not in terms of the order in which these resources should be reconciled. Kubernetes
operators typically need to reconcile resources in order because these resources’ state often
depends on the state of other resources or cannot be processed until these other resources reach
a given state or some condition holds true for them. Dealing with such scenarios are therefore
rather common for operators and the purpose of the workflow feature of the Java Operator SDK
(JOSDK) is to simplify supporting such cases in a declarative way. Workflows build on top of the
dependent resources feature.
While dependent resources focus on how a given secondary resource should be reconciled,
workflows focus on orchestrating how these dependent resources should be reconciled.
Workflows describe how as a set of
dependent resources (DR) depend on one
another, along with the conditions that need to hold true at certain stages of the
reconciliation process.
Elements of Workflow
Dependent resource (DR) - are the resources being managed in a given reconciliation logic.
Depends-on relation - a B
DR depends on another A
DR if B
needs to be reconciled
after A
.
Reconcile precondition - is a condition on a given DR that needs to be become true before the
DR is reconciled. This also allows to define optional resources that would, for example, only be
created if a flag in a custom resource .spec
has some specific value.
Ready postcondition - is a condition on a given DR to prevent the workflow from
proceeding until the condition checking whether the DR is ready holds true
Delete postcondition - is a condition on a given DR to check if the reconciliation of
dependents can proceed after the DR is supposed to have been deleted
Activation condition - is a special condition meant to specify under which condition the DR is used in the
workflow. A typical use-case for this feature is to only activate some dependents depending on the presence of
optional resources / features on the target cluster. Without this activation condition, JOSDK would attempt to
register an informer for these optional resources, which would cause an error in the case where the resource is
missing. With this activation condition, you can now conditionally register informers depending on whether the
condition holds or not. This is a very useful feature when your operator needs to handle different flavors of the
platform (e.g. OpenShift vs plain Kubernetes) and/or change its behavior based on the availability of optional
resources / features (e.g. CertManager, a specific Ingress controller, etc.).
A generic activation condition is provided out of the box, called
CRDPresentActivationCondition
that will prevent the associated dependent resource from being activated if the Custom Resource Definition associated
with the dependent’s resource type is not present on the cluster.
See related integration test.
To have multiple resources of same type with an activation condition is a bit tricky, since you
don’t want to have multiple InformerEventSource
for the same type, you have to explicitly
name the informer for the Dependent Resource (@KubernetesDependent(informerConfig = @InformerConfig(name = "configMapInformer"))
)
for all resource of same type with activation condition. This will make sure that only one is registered.
See details at low level api.
Result conditions
While simple conditions are usually enough, it might happen you want to convey extra information as a result of the
evaluation of the conditions (e.g., to report error messages or because the result of the condition evaluation might be
interesting for other purposes). In this situation, you should implement DetailedCondition
instead of Condition
and
provide an implementation of the detailedIsMet
method, which allows you to return a more detailed Result
object via
which you can provide extra information. The DetailedCondition.Result
interface provides factory method for your
convenience but you can also provide your own implementation if required.
You can access the results for conditions from the WorkflowResult
instance that is returned whenever a workflow is
evaluated. You can access that result from the ManagedWorkflowAndDependentResourceContext
accessible from the
reconciliation Context
. You can then access individual condition results using the getDependentConditionResult
methods. You can see an example of this
in this integration test.
Defining Workflows
Similarly to dependent resources, there are two ways to define workflows, in managed and standalone
manner.
Managed
Annotations can be used to declaratively define a workflow for a Reconciler
. Similarly to how
things are done for dependent resources, managed workflows execute before the reconcile
method
is called. The result of the reconciliation can be accessed via the Context
object that is
passed to the reconcile
method.
The following sample shows a hypothetical use case to showcase all the elements: the primary
TestCustomResource
resource handled by our Reconciler
defines two dependent resources, a
Deployment
and a ConfigMap
. The ConfigMap
depends on the Deployment
so will be
reconciled after it. Moreover, the Deployment
dependent resource defines a ready
post-condition, meaning that the ConfigMap
will not be reconciled until the condition defined
by the Deployment
becomes true
. Additionally, the ConfigMap
dependent also defines a
reconcile pre-condition, so it also won’t be reconciled until that condition becomes true
. The
ConfigMap
also defines a delete post-condition, which means that the workflow implementation
will only consider the ConfigMap
deleted until that post-condition becomes true
.
@Workflow(dependents = {
@Dependent(name = DEPLOYMENT_NAME, type = DeploymentDependentResource.class,
readyPostcondition = DeploymentReadyCondition.class),
@Dependent(type = ConfigMapDependentResource.class,
reconcilePrecondition = ConfigMapReconcileCondition.class,
deletePostcondition = ConfigMapDeletePostCondition.class,
activationCondition = ConfigMapActivationCondition.class,
dependsOn = DEPLOYMENT_NAME)
})
@ControllerConfiguration
public class SampleWorkflowReconciler implements Reconciler<WorkflowAllFeatureCustomResource>,
Cleaner<WorkflowAllFeatureCustomResource> {
public static final String DEPLOYMENT_NAME = "deployment";
@Override
public UpdateControl<WorkflowAllFeatureCustomResource> reconcile(
WorkflowAllFeatureCustomResource resource,
Context<WorkflowAllFeatureCustomResource> context) {
resource.getStatus()
.setReady(
context.managedWorkflowAndDependentResourceContext() // accessing workflow reconciliation results
.getWorkflowReconcileResult()
.allDependentResourcesReady());
return UpdateControl.patchStatus(resource);
}
@Override
public DeleteControl cleanup(WorkflowAllFeatureCustomResource resource,
Context<WorkflowAllFeatureCustomResource> context) {
// emitted code
return DeleteControl.defaultDelete();
}
}
Standalone
In this mode workflow is built manually
using standalone dependent resources
. The workflow is created using a builder, that is explicitly called in the reconciler (from web
page sample):
@ControllerConfiguration(
labelSelector = WebPageDependentsWorkflowReconciler.DEPENDENT_RESOURCE_LABEL_SELECTOR)
public class WebPageDependentsWorkflowReconciler
implements Reconciler<WebPage>, ErrorStatusHandler<WebPage> {
public static final String DEPENDENT_RESOURCE_LABEL_SELECTOR = "!low-level";
private static final Logger log =
LoggerFactory.getLogger(WebPageDependentsWorkflowReconciler.class);
private KubernetesDependentResource<ConfigMap, WebPage> configMapDR;
private KubernetesDependentResource<Deployment, WebPage> deploymentDR;
private KubernetesDependentResource<Service, WebPage> serviceDR;
private KubernetesDependentResource<Ingress, WebPage> ingressDR;
private final Workflow<WebPage> workflow;
public WebPageDependentsWorkflowReconciler(KubernetesClient kubernetesClient) {
initDependentResources(kubernetesClient);
workflow = new WorkflowBuilder<WebPage>()
.addDependentResource(configMapDR)
.addDependentResource(deploymentDR)
.addDependentResource(serviceDR)
.addDependentResource(ingressDR).withReconcilePrecondition(new ExposedIngressCondition())
.build();
}
@Override
public Map<String, EventSource> prepareEventSources(EventSourceContext<WebPage> context) {
return EventSourceUtils.nameEventSources(
configMapDR.initEventSource(context),
deploymentDR.initEventSource(context),
serviceDR.initEventSource(context),
ingressDR.initEventSource(context));
}
@Override
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context) {
var result = workflow.reconcile(webPage, context);
webPage.setStatus(createStatus(result));
return UpdateControl.patchStatus(webPage);
}
// omitted code
}
Workflow Execution
This section describes how a workflow is executed in details, how the ordering is determined and
how conditions and errors affect the behavior. The workflow execution is divided in two parts
similarly to how Reconciler
and Cleaner
behavior are separated.
Cleanup is
executed if a resource is marked for deletion.
Common Principles
- As complete as possible execution - when a workflow is reconciled, it tries to reconcile as
many resources as possible. Thus, if an error happens or a ready condition is not met for a
resources, all the other independent resources will be still reconciled. This is the opposite
to a fail-fast approach. The assumption is that eventually in this way the overall state will
converge faster towards the desired state than would be the case if the reconciliation was
aborted as soon as an error occurred.
- Concurrent reconciliation of independent resources - the resources which doesn’t depend on
others are processed concurrently. The level of concurrency is customizable, could be set to
one if required. By default, workflows use the executor service
from ConfigurationService
Reconciliation
This section describes how a workflow is executed, considering first which rules apply, then
demonstrated using examples:
Rules
- A workflow is a Directed Acyclic Graph (DAG) build from the DRs and their associated
depends-on
relations. - Root nodes, i.e. nodes in the graph that do not depend on other nodes are reconciled first,
in a parallel manner.
- A DR is reconciled if it does not depend on any other DRs, or ALL the DRs it depends on are
reconciled and ready. If a DR defines a reconcile pre-condition and/or an activation condition,
then these condition must become
true
before the DR is reconciled. - A DR is considered ready if it got successfully reconciled and any ready post-condition it
might define is
true
. - If a DR’s reconcile pre-condition is not met, this DR is deleted. All the DRs that depend
on the dependent resource are also recursively deleted. This implies that
DRs are deleted in reverse order compared the one in which they are reconciled. The reason
for this behavior is (Will make a more detailed blog post about the design decision, much deeper
than the reference documentation)
The reasoning behind this behavior is as follows: a DR with a reconcile pre-condition is only
reconciled if the condition holds
true
. This means that if the condition is false
and the
resource didn’t exist already, then the associated resource would not be created. To ensure
idempotency (i.e. with the same input state, we should have the same output state), from this
follows that if the condition doesn’t hold true
anymore, the associated resource needs to
be deleted because the resource shouldn’t exist/have been created. - If a DR’s activation condition is not met, it won’t be reconciled or deleted. If other DR’s depend on it, those will
be recursively deleted in a way similar to reconcile pre-conditions. Event sources for a dependent resource with
activation condition are registered/de-registered dynamically, thus during the reconciliation.
- For a DR to be deleted by a workflow, it needs to implement the
Deleter
interface, in which
case its delete
method will be called, unless it also implements the GarbageCollected
interface. If a DR doesn’t implement Deleter
it is considered as automatically deleted. If
a delete post-condition exists for this DR, it needs to become true
for the workflow to
consider the DR as successfully deleted.
Samples
Notation: The arrows depicts reconciliation ordering, thus following the reverse direction of the
depends-on
relation:
1 --> 2
mean DR 2
depends-on DR 1
.
Reconcile Sample
stateDiagram-v2
1 --> 2
1 --> 3
2 --> 4
3 --> 4
- Root nodes (i.e. nodes that don’t depend on any others) are reconciled first. In this example,
DR
1
is reconciled first since it doesn’t depend on others.
After that both DR 2
and 3
are reconciled concurrently, then DR 4
once both are
reconciled successfully. - If DR
2
had a ready condition and if it evaluated to as false
, DR 4
would not be reconciled.
However 1
,2
and 3
would be. - If
1
had a false
ready condition, neither 2
,3
or 4
would be reconciled. - If
2
’s reconciliation resulted in an error, 4
would not be reconciled, but 3
would be (and 1
as well, of course).
Sample with Reconcile Precondition
stateDiagram-v2
1 --> 2
1 --> 3
3 --> 4
3 --> 5
- If
3
has a reconcile pre-condition that is not met, 1
and 2
would be reconciled. However,
DR 3
,4
,5
would be deleted: 4
and 5
would be deleted concurrently but 3
would only
be deleted if 4
and 5
were deleted successfully (i.e. without error) and all existing
delete post-conditions were met. - If
5
had a delete post-condition that was false
, 3
would not be deleted but 4
would still be because they don’t depend on one another. - Similarly, if
5
’s deletion resulted in an error, 3
would not be deleted but 4
would be.
Cleanup
Cleanup works identically as delete for resources in reconciliation in case reconcile pre-condition
is not met, just for the whole workflow.
Rules
- Delete is called on a DR if there is no DR that depends on it
- If a DR has DRs that depend on it, it will only be deleted if all these DRs are successfully
deleted without error and any delete post-condition is
true
. - A DR is “manually” deleted (i.e. it’s
Deleter.delete
method is called) if it implements the
Deleter
interface but does not implement GarbageCollected
. If a DR does not implement
Deleter
interface, it is considered as deleted automatically.
Sample
stateDiagram-v2
1 --> 2
1 --> 3
2 --> 4
3 --> 4
- The DRs are deleted in the following order:
4
is deleted first, then 2
and 3
are deleted
concurrently, and, only after both are successfully deleted, 1
is deleted. - If
2
had a delete post-condition that was false
, 1
would not be deleted. 4
and 3
would be deleted. - If
2
was in error, DR 1
would not be deleted. DR 4
and 3
would be deleted. - if
4
was in error, no other DR would be deleted.
Error Handling
As mentioned before if an error happens during a reconciliation, the reconciliation of other
dependent resources will still happen, assuming they don’t depend on the one that failed. If
case multiple DRs fail, the workflow would throw an
‘AggregatedOperatorException’
containing all the related exceptions.
The exceptions can be handled
by ErrorStatusHandler
Waiting for the actual deletion of Kubernetes Dependent Resources
Let’s consider a case when a Kubernetes Dependent Resources (KDR) depends on another resource, on cleanup
the resources will be deleted in reverse order, thus the KDR will be deleted first.
However, the workflow implementation currently simply asks the Kubernetes API server to delete the resource. This is,
however, an asynchronous process, meaning that the deletion might not occur immediately, in particular if the resource
uses finalizers that block the deletion or if the deletion itself takes some time. From the SDK’s perspective, though,
the deletion has been requested and it moves on to other tasks without waiting for the resource to be actually deleted
from the server (which might never occur if it uses finalizers which are not removed).
In situations like these, if your logic depends on resources being actually removed from the cluster before a
cleanup workflow can proceed correctly, you need to block the workflow progression using a delete post-condition that
checks that the resource is actually removed or that it, at least, doesn’t have any finalizers any longer. JOSDK
provides such a delete post-condition implementation in the form of
KubernetesResourceDeletedCondition
Also, check usage in an integration test.
In such cases the Kubernetes Dependent Resource should extend CRUDNoGCKubernetesDependentResource
and NOT CRUDKubernetesDependentResource
since otherwise the Kubernetes Garbage Collector would delete the resources.
In other words if a Kubernetes Dependent Resource depends on another dependent resource, it should not implement
GargageCollected
interface, otherwise the deletion order won’t be guaranteed.
Explicit Managed Workflow Invocation
Managed workflows, i.e. ones that are declared via annotations and therefore completely managed by JOSDK, are reconciled
before the primary resource. Each dependent resource that can be reconciled (according to the workflow configuration)
will therefore be reconciled before the primary reconciler is called to reconcile the primary resource. There are,
however, situations where it would be be useful to perform additional steps before the workflow is reconciled, for
example to validate the current state, execute arbitrary logic or even skip reconciliation altogether. Explicit
invocation of managed workflow was therefore introduced to solve these issues.
To use this feature, you need to set the explicitInvocation
field to true
on the @Workflow
annotation and then
call the reconcileManagedWorkflow
method from the ManagedWorkflowAndDependentResourceContext
retrieved from the reconciliation Context
provided as part of your primary
resource reconciler reconcile
method arguments.
See
related integration test
for more details.
For cleanup
, if the Cleaner
interface is implemented, the cleanupManageWorkflow()
needs to be called explicitly.
However, if Cleaner
interface is not implemented, it will be called implicitly.
See
related integration test.
While nothing prevents calling the workflow multiple times in a reconciler, it isn’t typical or even recommended to do
so. Conversely, if explicit invocation is requested but reconcileManagedWorkflow
is not called in the primary resource
reconciler, the workflow won’t be reconciled at all.
Notes and Caveats
- Delete is almost always called on every resource during the cleanup. However, it might be the case
that the resources were already deleted in a previous run, or not even created. This should
not be a problem, since dependent resources usually cache the state of the resource, so are
already aware that the resource does not exist and that nothing needs to be done if delete is
called.
- If a resource has owner references, it will be automatically deleted by the Kubernetes garbage
collector if the owner resource is marked for deletion. This might not be desirable, to make
sure that delete is handled by the workflow don’t use garbage collected kubernetes dependent
resource, use for
example
CRUDNoGCKubernetesDependentResource
. - No state is persisted regarding the workflow execution. Every reconciliation causes all the
resources to be reconciled again, in other words the whole workflow is again evaluated.
8 - Architecture and Internals
This document provides an overview of the Java Operator SDK’s internal structure and components to help developers understand and contribute to the project. While not a comprehensive reference, it introduces core concepts that should make other components easier to understand.
The Big Picture and Core Components

An Operator is a set of independent controllers.
The Controller
class is an internal class managed by the framework and typically shouldn’t be interacted with directly. It manages all processing units involved with reconciling a single type of Kubernetes resource.
Core Components
- Reconciler - The primary entry point for developers to implement reconciliation logic
- EventSource - Represents a source of events that might trigger reconciliation
- EventSourceManager - Aggregates all event sources for a controller and manages their lifecycle
- ControllerResourceEventSource - Central event source that watches primary resources associated with a given controller for changes, propagates events and caches state
- EventProcessor - Processes incoming events sequentially per resource while allowing concurrent overall processing. Handles rescheduling and retrying
- ReconcilerDispatcher - Dispatches requests to appropriate
Reconciler
methods and handles reconciliation results, making necessary Kubernetes API calls
Typical Workflow
A typical workflow follows these steps:
- Event Generation: An
EventSource
produces an event and propagates it to the EventProcessor
- Resource Reading: The resource associated with the event is read from the internal cache
- Reconciliation Submission: If the resource isn’t already being processed, a reconciliation request is submitted to the executor service in a different thread (encapsulated in a
ControllerExecution
instance) - Dispatching: The
ReconcilerDispatcher
is called, which dispatches the call to the appropriate Reconciler
method with all required information - Reconciler Execution: Once the
Reconciler
completes, the ReconcilerDispatcher
makes appropriate Kubernetes API server calls based on the returned result - Finalization: The
EventProcessor
is called back to finalize execution and update the controller’s state - Rescheduling Check: The
EventProcessor
checks if the request needs rescheduling or retrying, and whether subsequent events were received for the same resource - Completion: When no further action is needed, event processing is finished