Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
02dadab
improve: add license headers to source files (#2980)
csviri Oct 9, 2025
319355d
chore: version to 5.3.0-SNAPSHOT
csviri Oct 14, 2025
eb4b21b
Annotation removal using locking (#3015)
shawkins Oct 30, 2025
f155074
improve: complete comparable resource version configs (#3027)
csviri Nov 13, 2025
678eafe
improve: run pr-s checks for v5.3 (#3042)
csviri Nov 17, 2025
eb205ac
fix: rebase on main after release
csviri Dec 1, 2025
bf1b996
fix(javadoc): invalid method ref blocks snapshot release (#3076)
csviri Dec 1, 2025
ecd9988
feat: record desired state in Context (#3082)
metacosm Dec 3, 2025
64c60e7
improve: rename junit5 module to junit (#3081)
csviri Dec 4, 2025
9a4ad30
fix: delete empty files result of rebase on main (#3093)
csviri Dec 15, 2025
fa6049b
feat: ReconcileUtils for strongly consistent updates (#3106)
csviri Jan 15, 2026
2b4795f
Event filtering now records resource action and previous resource (#3…
csviri Jan 21, 2026
120a28b
improve: facelift samples to use ReconcileUtils (#3135)
csviri Jan 27, 2026
4d384fd
improve: move compare resource version methods to internal utils (#3137)
csviri Jan 28, 2026
a28d7b4
feat: move ReconcileUtils methods to ResourceOperations accessible fr…
csviri Feb 2, 2026
59da824
improve: KubernetesDependentResource uses resource operations directl…
csviri Feb 2, 2026
9faeff4
feat: provide de-duplicated secondary resources stream on Context (#3…
metacosm Feb 3, 2026
bf394a1
refactor: avoid creating intermediate collections when unneeded (#3156)
metacosm Feb 5, 2026
1e09e29
improve: event filtering algorithm for multiple parallel updates (#3155)
csviri Feb 6, 2026
a33dbf7
improve: prepare for removal of exitOnStopLeading from public API (#3…
metacosm Feb 6, 2026
4d43730
fix: typo (#3173)
metacosm Feb 19, 2026
76614e1
fix: incorrect logic by introducing createOrUpdate method (#3172)
metacosm Feb 19, 2026
d078452
chore: set next version to 999-SNAPSHOT (#3180)
metacosm Feb 23, 2026
8b768ee
feat: allow to skip namespace deletion in junit extension (#3178)
csviri Feb 23, 2026
1c24e0d
improve: logging for resource filter cache (#3167)
csviri Feb 23, 2026
2226239
fix: unify how resource information is added, prevent NPEs (#3185)
metacosm Feb 25, 2026
8144ddf
feat: emit MDCUtils.NO_NAMESPACE value when namespace is null (#3186)
metacosm Feb 26, 2026
4868613
improve: do not close infra client if same as client (#3187)
csviri Feb 26, 2026
0afd4da
feat: add MDC to workflow execution (#3188)
csviri Feb 28, 2026
f465579
wip
csviri Mar 1, 2026
942ef64
wip
csviri Mar 5, 2026
4ee62d8
wip
csviri Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ on:
paths-ignore:
- 'docs/**'
- 'adr/**'
branches: [ main, next ]
branches: [ main, next, v5.3 ]
push:
paths-ignore:
- 'docs/**'
- 'adr/**'
branches:
- main
- next
- v5.3

jobs:
sample_operators_tests:
Expand Down
2 changes: 1 addition & 1 deletion bootstrapper-maven-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>java-operator-sdk</artifactId>
<version>5.2.4-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>bootstrapper</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
</dependency>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-junit-5</artifactId>
<artifactId>operator-framework-junit</artifactId>
<version>${josdk.version}</version>
<scope>test</scope>
</dependency>
Expand Down
4 changes: 2 additions & 2 deletions caffeine-bounded-cache-support/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>java-operator-sdk</artifactId>
<version>5.2.4-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>caffeine-bounded-cache-support</artifactId>
Expand All @@ -43,7 +43,7 @@
</dependency>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-junit-5</artifactId>
<artifactId>operator-framework-junit</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
Expand Down
145 changes: 145 additions & 0 deletions docs/content/en/blog/news/read-after-write-consistency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
title: Welcome read-cache-after-write consistency!
# todo issue with this?
#date: 2026-03-25
author: >-
[Attila Mészáros](https://github.com/csviri)
---

**TL;DR:**
In version 5.3.0 we introduced strong consistency guarantees for updates.
You can now update resources (both your custom resoure and managed resource)
and the framwork will guaratee that these updates will be instantly visible,
thus when accessing resources from caches;
and naturally also for subsequent reconciliations.

I briefly [talked about this](https://www.youtube.com/watch?v=HrwHh5Yh6AM&t=1387s) topic at KubeCon last year.

```java

public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context) {

ConfigMap managedConfigMap = prepareConfigMap(webPage);
// apply the resource with new API
context.resourceOperations().serverSideApply(managedConfigMap);

// fresh resource instantly available from our update in the caches
var upToDateResource = context.getSecondaryResource(ConfigMap.class);

// from now on built in update methods by default use this feature;
// it is guaranteed that resource changes will be visible for next reconciliation
return UpdateControl.patchStatus(alterStatusObject(webPage));
}
```

In addition to that framework will automatically filter events for your own updates,
so those are not triggering the reconciliation again.

{{% alert color=success %}}
**This should significantly simplify controller development, and will make reconciliation
much simpler to reason about!**
{{% /alert %}}

This post will deep dive in this topic, explore the details and rationale behind it.

## Informers and eventual consistency

First we have to understand a fundamental building block of Kubernetes operators: Informers.
Since there is plentiful accessible information about this topic, just in a nutshell, informers:

1. Watches Kubernetes resources - K8S API sends events if a resource changes to the client
though a websocket, An event usually contains the whole resource. (There are some exceptions, see Bookmarks).
See details about watch as K8S API concept in the [official docs](https://kubernetes.io/docs/reference/using-api/api-concepts/#semantics-for-watch).
2. Caches the actual latest state of the resource.
3. If an informer receives and event in which the `metadata.resourceVersion` is different from the version
in the cached resource it call the event handled, thus in our case triggers the reconciliation.

A controller is usually composed of multiple informers, one is tracking the primary resources, and
there are also informers registered for each (secondary) resource we manage.
Informers are great since we don't have to poll the Kubernetes API, it is push based; and they provide
a cache, so reconciliations are very fast since they work on top of cached resources.

Now let's take a look on the flow when we do an update to a resources:


```mermaid
graph LR
subgraph Controller
Informer:::informer
Cache[(Cache)]:::teal
Reconciler:::reconciler
Informer -->|stores| Cache
Reconciler -->|reads| Cache
end
K8S[⎈ Kubernetes API Server]:::k8s

Informer -->|watches| K8S
Reconciler -->|updates| K8S

classDef informer fill:#C0527A,stroke:#8C3057,color:#fff
classDef reconciler fill:#E8873A,stroke:#B05E1F,color:#fff
classDef teal fill:#3AAFA9,stroke:#2B807B,color:#fff
classDef k8s fill:#326CE5,stroke:#1A4AAF,color:#fff
```

It is easy to see that, the cache of the informer is eventually consistent with the update we sent from the reconciler.
It usually just takes a very short time (few milliseconds) to sync the caches until everything is ok. Well, sometimes
it is not. Websocket can be disconnected (actually happens on purpose sometimes), the API Server is slow etc.


## The problem(s) we try to solve

Let's consider the following operator:
- we have a custom resource `PodPrefix` where the spec contains only one field: `podNamePrexix`,
- goal of the operator is to create a pod with name that has the prefix and a random sequence
- it should never run two pods at once, if the `podNamePrefix` changes it should delete
the actual pod and after that create a new one
- the status of the custom resource should contain the `generatedPodName`

How the code would look like in 5.2.x:

```java

public UpdateControl<PodPrefix> reconcile(PodPrefix primary, Context<PodPrefix> context) {

Optional<Pod> currentPod = context.getSecondaryResource(Pod.class);

if (currentPod.isPresent()) {
if (podNameHasPrefix(primary.getSpec().getPodNamePrexix() ,currentPod.get())) {
// all ok we can return
return UpdateControl.noUpdate();
} else {
// deletes the current pod with different name pattern
context.getClient().resource(currentPod.get()).delete();
// it returns pod delete event will trigger the reconciliation
return UpdateControl.noUpdate();
}
} else {
// creates new pod
var newPod = context.getClient().resource(createPodWithOwnerReference(primary)).serviceSideApply();
return UpdateControl.patchStatus(setPodNameToStatus(primary,newPod));
}
}

@Override
public List<EventSource<?, WebPage>> prepareEventSources(EventSourceContext<WebPage> context) {

// Code omitted for adding InformerEventsSource for the pod


}
```

That is quite simple if there is a pod with different name prefix we delete it, otherwise we create the pod
and update the status. The pod is created with an owner reference so any update on pod will trigger
the reconciliation.

Now consider the following sequence of events:

1. We create a `PodPrefix` with `podNamePrefix`: "first-pod-prefix".
2. Concurrently:
- The reconciliation logic runs and creates a Pod with a name generated suffix: "first-pod-prefix-a3j3ka";
also sets this to the status and updates the custom resource status.
- While the reconciliation is running we update the custom resource to have the value
"second-pod-prefix"
3. The update of the custom resource triggers the reconciliation.
29 changes: 29 additions & 0 deletions docs/content/en/docs/documentation/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,35 @@ parts of reconciliation logic and during the execution of the controller:

For more information about MDC see this [link](https://www.baeldung.com/mdc-in-log4j-2-logback).

### MDC entries during event handling

Although, usually users might not require it in their day-to-day workflow, it is worth mentioning that
there are additional MDC entries managed for event handling. Typically, you might be interested in it
in your `SecondaryToPrimaryMapper` related logs.
For `InformerEventSource` and `ControllerEventSource` the following information is present:

| MDC Key | Value from Resource from the Event |
|:-----------------------------------------------|:-------------------------------------------------|
| `eventsource.event.resource.name` | `.metadata.name` |
| `eventsource.event.resource.uid` | `.metadata.uid` |
| `eventsource.event.resource.namespace` | `.metadata.namespace` |
| `eventsource.event.resource.kind` | resource kind |
| `eventsource.event.resource.resourceVersion` | `.metadata.resourceVersion` |
| `eventsource.event.action` | action name (e.g. `ADDED`, `UPDATED`, `DELETED`) |
| `eventsource.name` | name of the event source |

### Note on null values

If a resource doesn't provide values for one of the specified keys, the key will be omitted and not added to the MDC
context. There is, however, one notable exception: the resource's namespace, where, instead of omitting the key, we emit
the `MDCUtils.NO_NAMESPACE` value instead. This allows searching for resources without namespace (notably, clustered
resources) in the logs more easily.

### Disabling MDC support

MDC support is enabled by default. If you want to disable it, you can set the `JAVA_OPERATOR_SDK_USE_MDC` environment
variable to `false` when you start your operator.

## Metrics

JOSDK provides built-in support for metrics reporting on what is happening with your reconcilers in the form of
Expand Down
29 changes: 29 additions & 0 deletions docs/content/en/docs/migration/v5-3-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
title: Migrating from v5.2 to v5.3
description: Migrating from v5.2 to v5.3
---


## Renamed JUnit Module

If you use JUnit extension in your test just rename it from:

```
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-junit-5</artifactId>
<version>5.2.x<version>
<scope>test</scope>
</dependency>
```

to

```
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-junit</artifactId>
<version>5.3.0<version>
<scope>test</scope>
</dependency>
```
4 changes: 2 additions & 2 deletions micrometer-support/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>java-operator-sdk</artifactId>
<version>5.2.4-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>micrometer-support</artifactId>
Expand Down Expand Up @@ -58,7 +58,7 @@
</dependency>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-junit-5</artifactId>
<artifactId>operator-framework-junit</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
Expand Down
4 changes: 2 additions & 2 deletions operator-framework-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-bom</artifactId>
<version>5.2.4-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Operator SDK - Bill of Materials</name>
<description>Java SDK for implementing Kubernetes operators</description>
Expand Down Expand Up @@ -77,7 +77,7 @@
</dependency>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-junit-5</artifactId>
<artifactId>operator-framework-junit</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
Expand Down
2 changes: 1 addition & 1 deletion operator-framework-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>java-operator-sdk</artifactId>
<version>5.2.4-SNAPSHOT</version>
<version>999-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ public <P extends HasMetadata> RegisteredController<P> register(
"Cannot register reconciler with name "
+ reconciler.getClass().getCanonicalName()
+ " reconciler named "
+ ReconcilerUtils.getNameFor(reconciler)
+ ReconcilerUtilsInternal.getNameFor(reconciler)
+ " because its configuration cannot be found.\n"
+ " Known reconcilers are: "
+ configurationService.getKnownReconcilerNames());
Expand Down
Loading