Version 5.4 Released!
We’re pleased to announce the release of Java Operator SDK v5.4.0! This minor version adds
sharding support for horizontally splitting a workload across operator replicas, finer-grained
control over event filtering, richer secondary-resource lookups on Context, and a smarter retry
scheduler — along with a number of smaller improvements, deprecations, and a Fabric8 client upgrade.
Key Features
Shard Selector Support
Large clusters sometimes need the same operator to run as multiple replicas, each responsible for a subset (“shard”) of the resources. From 5.4.0 you can configure a shard selector — a second, Kubernetes-style label selector that is applied in addition to the normal label selector (the two are combined with logical AND).
@ControllerConfiguration(informer = @Informer(shardSelector = "shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000')"))
public class MyReconciler implements Reconciler<MyCustomResource> { ... }
It can also be set programmatically or via configuration:
ControllerConfigurationOverrider.override(config)
.withShardSelector("shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000')")
.build();
withShardSelector(String) is available on InformerConfiguration.Builder,
InformerEventSourceConfiguration.Builder, and ControllerConfigurationOverrider, and via the
josdk.controller.<name>.shard-selector configuration key. This feature relies on Fabric8 client
support and requires the 7.8.0 baseline shipped with this release.
Opting Out of Default Event Filters
By default, JOSDK applies a set of internal update filters to a controller’s own event source: generation-aware filtering, and finalizer-needed/marked-for-deletion handling. These are the right default for most operators, but occasionally you need full control over exactly which updates trigger a reconciliation.
The new defaultFilters flag (default true) lets you turn them off. When set to false, your
@Informer(onUpdateFilter = ...) becomes the sole update filter — and if you don’t set one, all
updates pass through:
@ControllerConfiguration(
defaultFilters = false,
informer = @Informer(onUpdateFilter = MyUpdateFilter.class))
public class MyReconciler implements Reconciler<MyCustomResource> { ... }
The internal filter building blocks in InternalEventFilters are now public, so you can re-compose
the parts you still want alongside your custom logic:
OnUpdateFilter<MyCustomResource> composed =
InternalEventFilters.<MyCustomResource>onUpdateGenerationAware(true)
.or((newRes, oldRes) -> /* custom trigger, e.g. a specific annotation present */);
withDefaultFilters(boolean) is also available on ControllerConfigurationOverrider.
By-name Secondary Resource Lookup on Context
Looking up a single secondary resource by name previously meant streaming all secondaries and
filtering them yourself. Context now offers direct by-name lookups:
// name + namespace from a specific named event source
Optional<Secret> secret =
context.getSecondaryResource(Secret.class, "my-event-source", "cred-secret", "my-ns");
// namespace inferred from the primary resource
Optional<Secret> secret2 =
context.getSecondaryResource(Secret.class, "my-event-source", "cred-secret");
// stream all secondaries of a type from a specific named event source
Stream<ConfigMap> configMaps =
context.getSecondaryResourcesAsStream(ConfigMap.class, "cm-event-source");
When the underlying event source is a cache, these hit the cache directly (and are read-cache-after-write
consistent); otherwise they fall back to filtering the full secondary set. The stream overload works
for non-Kubernetes secondary types too — its type bound was relaxed so it is no longer restricted
to HasMetadata — making it usable with external/polling event sources.
Retry Interval Honored Under Frequent Events
Previously, when a failed reconciliation was triggered by an incoming event while a retry was already scheduled, the incoming reconciliation could consume a retry attempt and advance the retry counter. Under a steady stream of external events this meant the configured retry interval was effectively ignored and the operator could exhaust its retries far too quickly.
From 5.4.0, an event-driven reconciliation that fails while there is still plenty of time left in the current retry window preserves the existing retry deadline and does not consume a retry attempt — it simply re-schedules on the original deadline. A retry attempt is only counted once the scheduled deadline is imminent (within 5 seconds). This is transparent to users; the configured retry interval is now genuinely honored even under frequent events.
A new RetryExecution#remainingDurationUntilNextRetry() returning Optional<Duration> supports this
behavior.
Reconciling Dropped Secondary References
On an update event for a secondary resource, the framework now invokes your
SecondaryToPrimaryMapper for both the old and the new version of the resource and reconciles
the union of the results. This means primaries that the secondary used to reference but no longer
does — including partial subset changes — are also reconciled and can revert to their expected state.
Note: Because the mapper may now be called on an older version of a resource, a
SecondaryToPrimaryMapperimplementation must be a pure function of the resource passed to it and must not rely on external “current” state.
Additional Improvements
- Owner-reference mappers match on group only:
Mappers.fromOwnerReferences(apiVersion, kind, ...)now matches on kind + group and ignores the version. Owner references written under one CRD version (e.g..../v1) still resolve correctly after the served/storage version changes (e.g. to.../v2). GenericRetry#setMaxInterval(Duration): aDuration-based overload alongside the existing millis-based one, e.g.new GenericRetry().setMaxInterval(Duration.ofMinutes(5)).
Migration Notes
Shutdown hook: installShutdownHook(Duration) deprecated
Operator#installShutdownHook(Duration) is deprecated for removal. Its Duration argument is now
ignored. This also fixes a deadlock that could occur when stop() was called from a JVM shutdown
hook while leader election was active.
// before
operator.installShutdownHook(Duration.ofSeconds(30));
// after — timeout comes from ConfigurationService
operator.installShutdownHook();
Configure the graceful shutdown timeout via
ConfigurationServiceOverrider#withReconciliationTerminationTimeout(Duration). Unlike the old
variant, the no-arg installShutdownHook() installs regardless of whether leader election is enabled,
and is idempotent.
Instance-based Mappers.fromOwnerReferences deprecated
The overloads taking a HasMetadata instance are deprecated for removal. Pass the primary
resource class instead:
// before
Mappers.fromOwnerReferences(primaryResource);
Mappers.fromOwnerReferences(primaryResource, clusterScoped);
// after
Mappers.fromOwnerReferences(MyPrimary.class);
Mappers.fromOwnerReferences(MyPrimary.class, clusterScoped);
Getting Started
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework</artifactId>
<version>5.4.0</version>
</dependency>
All Changes
See the comparison view for the full list of changes.
Feedback
Please report issues or suggest improvements on our GitHub repository.
Happy operator building! 🚀