-
Notifications
You must be signed in to change notification settings - Fork 242
next (v5.6; do not merge) #3474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
6d18a4b
chore: set next version to 999-SNAPSHOT (#3469)
csviri ae70009
improve: integration test to showcase external resource state in stat…
csviri 97909d4
Informer pools (#3325)
csviri 388afa2
fix: new kotlin sample parent version
csviri f37866c
fix: set configuration service for default pool (#3535)
csviri 63a30f6
perf: size the workflow result map from Workflow#size (#3547)
csviri d4346d2
refactor: call existing helpers instead of re-implementing them (#3544)
csviri d23a2fe
test: fix flaky finalizer removal in TriggerReconcilerOnAllEventIT (#…
csviri 6bb9e39
feat: detect dependent resource API version changes (#3536)
exijn 57df8aa
refactor: resolve the informer target client without a downcast (#3548)
csviri f172646
refactor: clean up the Kubernetes resource matchers (#3546)
csviri 412f8cf
improve: followup PR for Informer Pools (#3541)
csviri 065669e
refactor: let ResourceState own the trigger-on-all-events flag (#3549)
csviri c6040f3
perf: avoid redundant work on informer event paths (#3545)
csviri 19c3cd4
improve: log informer re-use on info level (#3569)
csviri 19629d0
test: cover informer retry after a CR deserialization problem (#3558)
csviri 0a365dc
fix: retain recently written external resources missing from a stale …
csviri 704bd0c
fix: small issue after rebase on main
csviri 877f094
Release 5.6 blog post (#3579)
csviri d11ffbd
improve: simplify log message for informer re-use (#3583)
csviri b2e2627
refactor: configurable event recorder (#3581)
csviri fd323b4
Apply batched suggestions from code review
csviri 478199e
docs: fix javadoc invalid reference for EventRecorder
csviri File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,245 @@ | ||
| --- | ||
| title: Version 5.6 Released! | ||
| date: 2026-09-02 | ||
| author: >- | ||
| [Attila Mészáros](https://github.com/csviri) | ||
| --- | ||
|
|
||
| We're pleased to announce the release of Java Operator SDK v5.6.0! The headline features of this | ||
| minor version are **informer pooling** — informers are now shared between controllers and event | ||
| sources instead of one per event source — and a first-class **event recorder** for writing Kubernetes | ||
| events. There are **no breaking API changes**. | ||
|
|
||
| ## Key Features | ||
|
|
||
| ### Informer pooling: shared informers across controllers | ||
|
|
||
| Until now every `InformerEventSource` created its own `SharedIndexInformer`. An operator whose | ||
| controllers all watch the same secondary type — `ConfigMap` and `Secret` being the usual suspects — | ||
| opened one watch connection and kept one full cache **per controller** for the very same resources. | ||
|
|
||
| Informers are now handed out by an `InformerPool` obtained from the `ConfigurationService`. Event | ||
| sources whose effective informer configuration is equivalent are backed by a single informer, which | ||
| cuts both memory usage and the number of watch connections opened against the API server. Two event | ||
| sources share an informer when they match on all of: | ||
|
|
||
| - the `KubernetesClient` they watch through, compared **by instance** (two clients are never | ||
| considered equivalent even when they point at the same API server — they may differ in | ||
| credentials, impersonation or TLS material), | ||
| - the resource type, or the group/version/kind for generic resources, | ||
| - the watched namespace, | ||
| - the label, field and shard selectors, | ||
| - the configured item store. | ||
|
|
||
| Two things are deliberately *not* part of that identity. The `informerListLimit` is excluded, so | ||
| event sources that disagree only on it still share an informer, keeping the limit of whichever one | ||
| created it and logging a warning. Indexers are excluded because they can be added to a running | ||
| informer: they are registered under a name qualified with the controller and event source that added | ||
| them, so index names stay private to an event source and cannot collide, and they are removed again | ||
| when that event source releases the informer. You keep looking up indexes by the name you | ||
| registered. | ||
|
|
||
| The pool is reference counted: the informer is created on first use and stopped only once the last | ||
| event source using it is de-registered (or its controller stops). Registering an event source | ||
| dynamically against an already running shared informer needs no special handling — the cache | ||
| contents are replayed to the newly added handler. | ||
|
|
||
| Two strategies ship, and both are selected through the `ConfigurationService`: | ||
|
|
||
| ```java | ||
| // opt out of sharing, restoring the pre-5.6 behavior of one informer per event source | ||
| Operator operator = new Operator(overrider -> | ||
| overrider.withInformerPool(new NonSharingInformerPool())); | ||
| ``` | ||
|
|
||
| `DefaultInformerPool` shares as described above and is the default; `NonSharingInformerPool` creates | ||
| a dedicated informer per event source. A custom strategy extends `AbstractInformerPool`, which | ||
| already creates the informers from an `InformerClassifier`, starts them and waits for their caches to | ||
| sync, leaving the subclass only the question of whether and when an informer is shared. | ||
|
|
||
| > **Note on stability**: the pooling itself is production ready, but the configuration API around it | ||
| > (`ConfigurationService#informerPool`, `withInformerPool`) is marked `@Experimental` and may still | ||
| > change in a non-backwards-compatible way. | ||
|
|
||
| See the [eventing documentation](/docs/documentation/eventing) for details. | ||
|
|
||
| ### Kubernetes event recorder | ||
|
|
||
| JOSDK now knows how to record Kubernetes events, so that a reconciler can surface what it is doing | ||
| where users already look for it — `kubectl describe`. Inside a reconciliation the recorder is | ||
| available from the `Context`, already bound to the primary resource: | ||
|
|
||
| ```java | ||
| @Override | ||
| public UpdateControl<MyResource> reconcile(MyResource resource, Context<MyResource> context) { | ||
| context.eventRecorder().normal("Reconciled", "resource reconciled"); | ||
| context.eventRecorder().warn("SomethingIsOff", "this is a warning about the resource"); | ||
| return UpdateControl.noUpdate(); | ||
| } | ||
| ``` | ||
|
|
||
| For more control, build an `EventRecord`, which also carries an optional action, reporting component, | ||
| labels and annotations: | ||
|
|
||
| ```java | ||
| context.eventRecorder().record(EventRecord.builder() | ||
| .type(EventType.WARNING) | ||
| .reason("DeploymentFailed") | ||
| .message("could not scale the deployment") | ||
| .action("Scaling") | ||
| .build()); | ||
| ``` | ||
|
|
||
| Outside of the reconciliation loop — from a status listener or a background task, or about an object | ||
| other than the primary — use the unbound form from | ||
| `RegisteredController#eventRecorder()`, which is scoped to the controller rather than to a | ||
| reconciliation: | ||
|
|
||
| ```java | ||
| EventRecorder recorder = registeredController.eventRecorder(); | ||
| recorder.record(someOtherResource, EventRecord.normal("Noticed", "something happened")); | ||
| ``` | ||
|
|
||
| Two properties are worth calling out: | ||
|
|
||
| - **Recording is best effort.** A failure to write the event is logged and swallowed, and never fails | ||
| the caller — a controller that fails to reconcile because it could not write an event is strictly | ||
| worse than one that records nothing. | ||
| - **Repeats are aggregated, not duplicated.** Events are named deterministically, after the object | ||
| they are about plus a hash of everything that identifies the event. Recording the same event again | ||
| therefore resolves to the event already recorded and patches its `count` and `lastTimestamp`, which | ||
| is what makes `kubectl describe` report a repeating event once as `(x12 over 3m)` instead of | ||
| filling the event list with copies. Use `EventRecord.Builder#key(...)` to control what counts as | ||
| "the same" event. | ||
|
|
||
| Events live in a namespace of their own: that of the object they are about, and for cluster scoped | ||
| objects the namespace configured via `withClusterScopedEventNamespace(...)` or the | ||
| `josdk.events.cluster-scoped-namespace` property, defaulting to `default`. | ||
|
|
||
| Recording needs `get`, `create` and `patch` on `events` in the core (`""`) API group — all three, | ||
| since an event is looked up before it is created and patched when it already exists. The generic Helm | ||
| chart's `ClusterRole` grants them out of the box. If you write your own RBAC, note that a namespaced | ||
| `Role` has to grant the permission in *every* namespace events are recorded in, including the | ||
| cluster-scoped event namespace. | ||
|
|
||
| Thanks to [Tan Qi](https://github.com/TQJADE) for contributing this feature! | ||
|
|
||
| ### Detecting dependent resource API version changes (experimental) | ||
|
|
||
| When a dependent resource's CRD gains a new API version and the operator is upgraded to target it, | ||
| comparing the actual resource's `apiVersion` with the desired one detects nothing: the API server | ||
| serves a resource under the requested version regardless of how it is stored, so the comparison | ||
| always trivially matches. `KubernetesDependentResource` consequently ignores `apiVersion` when | ||
| matching. | ||
|
|
||
| To still force a one-time update after such an upgrade, `@KubernetesDependent` gained an opt-in | ||
| `detectApiVersionChange` flag: | ||
|
|
||
| ```java | ||
| @KubernetesDependent(detectApiVersionChange = true) | ||
| public class MyDependentResource extends CRUDKubernetesDependentResource<ConfigMap, MyPrimary> { | ||
| // ... | ||
| } | ||
| ``` | ||
|
|
||
| When enabled, JOSDK records the API version it applies in the | ||
| `javaoperatorsdk.io/last-applied-api-version` annotation, and the matcher reports a mismatch when | ||
| that marker differs from the version currently in use — which also covers resources predating this | ||
| feature and therefore carrying no marker. After the update the marker matches again, so no | ||
| reconciliation loop results. It is disabled by default, and is not a replacement for Kubernetes' | ||
| [StorageVersionMigration](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/storage-version-migration/), | ||
| which addresses the orthogonal concern of migrating the stored representation. | ||
|
|
||
| Thanks to [한의준](https://github.com/hej090224) for this contribution! | ||
|
|
||
| ## Bug Fixes | ||
|
|
||
| ### Recently written external resources are no longer lost from the cache | ||
|
|
||
| An update of the whole resource set of a primary — a poll result or a received event — might have | ||
| been created *before* the reconciler wrote a resource, and therefore not contain it yet. Since such | ||
| updates are treated as the full actual state, the write was lost from the cache, and the next | ||
| reconciliation created a duplicate of an already created resource or repeated an already executed | ||
| update. | ||
|
|
||
| Writes are now marked as unconfirmed and retained for the next update if that update either does not | ||
| contain the resource at all (the expected case for a create) or still contains a state that a write | ||
| replaced. Every state replaced since the last update is kept, since the reconciler may write the same | ||
| resource several times in between. Any other state is treated as a change made outside of the | ||
| reconciler and accepted as actual, and marks are dropped on the first update, so a resource really | ||
| deleted or changed meanwhile is not retained indefinitely. | ||
|
|
||
| For external state bulk dependent resources that take longer to become visible, the recommended | ||
| approach remains to resolve the actual resources from the state resources in | ||
| `BulkDependentResource.getSecondaryResources` — the state resources are managed by an | ||
| `InformerEventSource` and are therefore always up-to-date regarding the operator's own changes. This | ||
| is now documented and shown in the | ||
| [integration test](https://github.com/operator-framework/java-operator-sdk/tree/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/externalstate/externalstatebulkdependent). | ||
|
|
||
| ## Additional Improvements | ||
|
|
||
| - **Less redundant work on informer event paths**: `Mappers#fromMetadata` no longer resolves the | ||
| primary `GroupVersionKind` on every secondary event, `SecondaryToPrimaryFromDefaultAnnotation` no | ||
| longer builds a whole new mapper per invocation, and `InformerEventSource#start` no longer walks | ||
| the entire informer cache to seed a primary-to-secondary index that is the no-op implementation | ||
| (i.e. whenever a `primaryToSecondaryMapper` is configured) — that walk was pure startup latency | ||
| proportional to the number of cached secondaries. | ||
| - **The JavaPoet dependency is gone.** It was only used by the annotation processor to turn a | ||
| resolved `TypeMirror` into its fully qualified name, which the standard annotation processing API | ||
| does on its own. JavaPoet has not been released since 2024, which made it a problem for users whose | ||
| organizations do not approve unmaintained dependencies. | ||
| - Internal clean-ups with no API impact: the Kubernetes resource matchers were simplified, the | ||
| informer target client is resolved without a downcast, `ResourceState` now owns the | ||
| trigger-on-all-events flag, and the workflow result map is sized from `Workflow#size`. | ||
| - Additional test coverage, including informer retry after a custom resource deserialization problem, | ||
| and integration tests for informer sharing, dynamic registration and de-registration against both | ||
| pool strategies. | ||
| - Dependency updates: Micrometer 1.17.1, JUnit BOM 6.1.3, Jetty 12.1.12, Apache Maven 3.9.16, | ||
| OpenRewrite 8.90.4 and others. | ||
|
|
||
| ## Migration Notes | ||
|
|
||
| There are **no breaking API changes**; existing code compiles and runs unchanged. Two things are | ||
| worth being aware of: | ||
|
|
||
| ### Informers are shared by default | ||
|
|
||
| This is a behavioral change: event sources with equivalent configuration now share one informer and | ||
| one cache. This should be transparent, but if your operator depends on having a dedicated informer | ||
| per event source, opt out with `withInformerPool(new NonSharingInformerPool())`. | ||
|
|
||
| ### `InformerEventSource(configuration, context)` is deprecated | ||
|
|
||
| Since the informer is now created by the pool from the configuration, the `EventSourceContext` is no | ||
| longer needed to resolve the client. The two-argument constructor is deprecated for removal; drop the | ||
| context argument: | ||
|
|
||
| ```java | ||
| // before | ||
| new InformerEventSource<>(configuration, context); | ||
|
|
||
| // after | ||
| new InformerEventSource<>(configuration); | ||
| ``` | ||
|
|
||
| ## Getting Started | ||
|
|
||
| ```xml | ||
| <dependency> | ||
| <groupId>io.javaoperatorsdk</groupId> | ||
| <artifactId>operator-framework</artifactId> | ||
| <version>5.6.0</version> | ||
| </dependency> | ||
| ``` | ||
|
|
||
| ## All Changes | ||
|
|
||
| See the [comparison view](https://github.com/operator-framework/java-operator-sdk/compare/v5.5.1...v5.6.0) | ||
| for the full list of changes. | ||
|
|
||
| ## Feedback | ||
|
|
||
| Please report issues or suggest improvements on our | ||
| [GitHub repository](https://github.com/operator-framework/java-operator-sdk/issues). | ||
|
|
||
| Happy operator building! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.