Skip to content

Commit 9a4578f

Browse files
committed
wip
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent 5b2fccd commit 9a4578f

7 files changed

Lines changed: 287 additions & 3 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
title: Version 5.5 Released!
3+
date: 2026-07-14
4+
author: >-
5+
[Attila Mészáros](https://github.com/csviri)
6+
---
7+
8+
We're pleased to announce the release of Java Operator SDK v5.5.0!
9+
10+
## Key Features
11+
12+
## Additional Improvements
13+
14+
## Migration Notes
15+
16+
## Getting Started
17+
18+
```xml
19+
<dependency>
20+
<groupId>io.javaoperatorsdk</groupId>
21+
<artifactId>operator-framework</artifactId>
22+
<version>5.5.0</version>
23+
</dependency>
24+
```
25+
26+
## All Changes
27+
28+
See the [comparison view](https://github.com/operator-framework/java-operator-sdk/compare/v5.4.0...v5.5.0)
29+
for the full list of changes.
30+
31+
## Feedback
32+
33+
Please report issues or suggest improvements on our
34+
[GitHub repository](https://github.com/operator-framework/java-operator-sdk/issues).
35+
36+
Happy operator building! 🚀

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperations.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ private <R extends HasMetadata> R resourcePatch(
733733

734734
public <R extends HasMetadata> R resourcePatch(
735735
R desired, UnaryOperator<R> updateOperation, ManagedInformerEventSource<R, P, ?> ies) {
736-
return resourcePatch(desired, updateOperation, Options.filterIfOptimisticLocking());
736+
return resourcePatch(desired, updateOperation, ies, Options.filterIfOptimisticLocking());
737737
}
738738

739739
public <R extends HasMetadata> R resourcePatch(
@@ -1031,10 +1031,12 @@ public boolean requiresMatcher() {
10311031
}
10321032

10331033
@Experimental(API_MIGHT_CHANGE)
1034+
/** */
10341035
public enum Mode {
10351036
FILTER_IF_OPTIMISTIC_LOCKING,
10361037
FILTER_IF_NOT_MATCHING,
10371038
CACHE_ONLY,
1039+
/** */
10381040
FORCE_FILTER,
10391041
}
10401042

operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/readcacheafterwrite/externalupdateduringownupdate/ExternalUpdateDuringOwnUpdateReconciler.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package io.javaoperatorsdk.operator.baseapi.readcacheafterwrite.externalupdateduringownupdate;
1717

1818
import java.util.concurrent.CountDownLatch;
19+
import java.util.concurrent.TimeUnit;
1920
import java.util.concurrent.atomic.AtomicBoolean;
2021
import java.util.concurrent.atomic.AtomicInteger;
2122

@@ -46,10 +47,28 @@ public UpdateControl<ExternalUpdateDuringOwnUpdateCustomResource> reconcile(
4647
if (execution == 1) {
4748
var status = new ExternalUpdateDuringOwnUpdateStatus().setValue(STATUS_VALUE);
4849
resource.setStatus(status);
50+
4951
// wrap our own status update in resourcePatch with a hook that lets the test
5052
// perform an external metadata update WHILE our filter window is still open.
51-
resource.getMetadata().setResourceVersion(null);
52-
context.resourceOperations().jsonMergePatchPrimary(resource);
53+
context
54+
.resourceOperations()
55+
.resourcePatch(
56+
resource,
57+
r -> {
58+
updateStartedLatch.countDown();
59+
try {
60+
if (!externalUpdateDoneLatch.await(30, TimeUnit.SECONDS)) {
61+
throw new RuntimeException("timed out waiting for external update");
62+
}
63+
} catch (InterruptedException e) {
64+
Thread.currentThread().interrupt();
65+
throw new RuntimeException(e);
66+
}
67+
// server-side state moved due to the external label change; drop our stale rv
68+
r.getMetadata().setResourceVersion(null);
69+
return context.getClient().resource(r).patchStatus();
70+
},
71+
context.eventSourceRetriever().getControllerEventSource());
5372
} else {
5473
var labels = resource.getMetadata().getLabels();
5574
if (labels != null && EXTERNAL_LABEL_VALUE.equals(labels.get(EXTERNAL_LABEL_KEY))) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.baseapi.readcacheafterwrite.ownssastatusupdate;
17+
18+
import io.fabric8.kubernetes.api.model.Namespaced;
19+
import io.fabric8.kubernetes.client.CustomResource;
20+
import io.fabric8.kubernetes.model.annotation.Group;
21+
import io.fabric8.kubernetes.model.annotation.ShortNames;
22+
import io.fabric8.kubernetes.model.annotation.Version;
23+
24+
@Group("sample.javaoperatorsdk")
25+
@Version("v1")
26+
@ShortNames("ossasu")
27+
public class OwnSsaStatusUpdateCustomResource extends CustomResource<Void, OwnSsaStatusUpdateStatus>
28+
implements Namespaced {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.baseapi.readcacheafterwrite.ownssastatusupdate;
17+
18+
import java.time.Duration;
19+
import java.util.HashMap;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.RegisterExtension;
23+
24+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
25+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
26+
27+
import static io.javaoperatorsdk.operator.baseapi.readcacheafterwrite.ownssastatusupdate.OwnSsaStatusUpdateReconciler.EXTERNAL_LABEL_KEY;
28+
import static io.javaoperatorsdk.operator.baseapi.readcacheafterwrite.ownssastatusupdate.OwnSsaStatusUpdateReconciler.EXTERNAL_LABEL_VALUE;
29+
import static io.javaoperatorsdk.operator.baseapi.readcacheafterwrite.ownssastatusupdate.OwnSsaStatusUpdateReconciler.STATUS_VALUE;
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
import static org.awaitility.Awaitility.await;
32+
33+
/**
34+
* Verifies that updating the status through {@code
35+
* resourceOperations().serverSideApplyPrimaryStatus(...)} (instead of returning an {@code
36+
* UpdateControl}) is read-cache-after-write consistent and does not loop: the own SSA status write
37+
* is filtered as an own event, so the controller converges. A subsequent external label change must
38+
* still be picked up by a fresh reconciliation.
39+
*/
40+
class OwnSsaStatusUpdateIT {
41+
42+
static final String RESOURCE_NAME = "test-resource";
43+
44+
OwnSsaStatusUpdateReconciler reconciler = new OwnSsaStatusUpdateReconciler();
45+
46+
@RegisterExtension
47+
LocallyRunOperatorExtension extension =
48+
LocallyRunOperatorExtension.builder().withReconciler(reconciler).build();
49+
50+
@Test
51+
void ssaStatusUpdateIsConsistentAndDoesNotLoop() {
52+
extension.create(testResource());
53+
54+
// the status is persisted via the own SSA status update
55+
await()
56+
.atMost(Duration.ofSeconds(30))
57+
.untilAsserted(
58+
() -> {
59+
var actual = extension.get(OwnSsaStatusUpdateCustomResource.class, RESOURCE_NAME);
60+
assertThat(actual.getStatus()).isNotNull();
61+
assertThat(actual.getStatus().getValue()).isEqualTo(STATUS_VALUE);
62+
});
63+
64+
// the own status write must be filtered: no reconciliation loop
65+
await()
66+
.during(Duration.ofSeconds(2))
67+
.atMost(Duration.ofSeconds(5))
68+
.untilAsserted(
69+
() ->
70+
assertThat(reconciler.numberOfExecutions.get())
71+
.as("own SSA status update must not trigger a reconciliation loop")
72+
.isLessThanOrEqualTo(2));
73+
74+
var executionsBeforeExternalUpdate = reconciler.numberOfExecutions.get();
75+
76+
// an external party changes a label; this must still trigger a fresh reconciliation
77+
var current = extension.get(OwnSsaStatusUpdateCustomResource.class, RESOURCE_NAME);
78+
var labels = new HashMap<String, String>();
79+
if (current.getMetadata().getLabels() != null) {
80+
labels.putAll(current.getMetadata().getLabels());
81+
}
82+
labels.put(EXTERNAL_LABEL_KEY, EXTERNAL_LABEL_VALUE);
83+
current.getMetadata().setLabels(labels);
84+
extension.replace(current);
85+
86+
await()
87+
.atMost(Duration.ofSeconds(30))
88+
.untilAsserted(
89+
() -> {
90+
assertThat(reconciler.numberOfExecutions.get())
91+
.isGreaterThan(executionsBeforeExternalUpdate);
92+
assertThat(reconciler.externalLabelSeenInLaterReconciliation.get())
93+
.as("a later reconciliation must observe the externally-applied label")
94+
.isTrue();
95+
});
96+
}
97+
98+
OwnSsaStatusUpdateCustomResource testResource() {
99+
var r = new OwnSsaStatusUpdateCustomResource();
100+
r.setMetadata(new ObjectMetaBuilder().withName(RESOURCE_NAME).build());
101+
return r;
102+
}
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.baseapi.readcacheafterwrite.ownssastatusupdate;
17+
18+
import java.util.concurrent.atomic.AtomicBoolean;
19+
import java.util.concurrent.atomic.AtomicInteger;
20+
21+
import io.javaoperatorsdk.operator.api.reconciler.Context;
22+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
23+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
24+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
25+
26+
/**
27+
* Updates its own status via {@link
28+
* io.javaoperatorsdk.operator.api.reconciler.ResourceOperations#serverSideApplyPrimaryStatus(
29+
* io.fabric8.kubernetes.api.model.HasMetadata)} instead of returning an {@link UpdateControl}. The
30+
* status is server-side applied through the controller's own event source, so the resulting own
31+
* event must be filtered and must not trigger a reconciliation loop. A later external update (a
32+
* label change) must still be observed by a fresh reconciliation.
33+
*/
34+
@ControllerConfiguration(generationAwareEventProcessing = false)
35+
public class OwnSsaStatusUpdateReconciler implements Reconciler<OwnSsaStatusUpdateCustomResource> {
36+
37+
static final String STATUS_VALUE = "ready";
38+
static final String EXTERNAL_LABEL_KEY = "externally-set";
39+
static final String EXTERNAL_LABEL_VALUE = "yes";
40+
41+
final AtomicInteger numberOfExecutions = new AtomicInteger();
42+
final AtomicBoolean externalLabelSeenInLaterReconciliation = new AtomicBoolean();
43+
44+
@Override
45+
public UpdateControl<OwnSsaStatusUpdateCustomResource> reconcile(
46+
OwnSsaStatusUpdateCustomResource resource,
47+
Context<OwnSsaStatusUpdateCustomResource> context) {
48+
numberOfExecutions.incrementAndGet();
49+
50+
var labels = resource.getMetadata().getLabels();
51+
if (labels != null && EXTERNAL_LABEL_VALUE.equals(labels.get(EXTERNAL_LABEL_KEY))) {
52+
externalLabelSeenInLaterReconciliation.set(true);
53+
}
54+
55+
// Only apply the status when it is not already set - the SSA status matcher makes repeated
56+
// applies no-ops anyway, but this keeps the intent explicit and the reconciliation idempotent.
57+
if (resource.getStatus() == null || !STATUS_VALUE.equals(resource.getStatus().getValue())) {
58+
resource.setStatus(new OwnSsaStatusUpdateStatus().setValue(STATUS_VALUE));
59+
// SSA does not use optimistic locking
60+
resource.getMetadata().setResourceVersion(null);
61+
context.resourceOperations().serverSideApplyPrimaryStatus(resource);
62+
}
63+
64+
return UpdateControl.noUpdate();
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.baseapi.readcacheafterwrite.ownssastatusupdate;
17+
18+
public class OwnSsaStatusUpdateStatus {
19+
20+
private String value;
21+
22+
public String getValue() {
23+
return value;
24+
}
25+
26+
public OwnSsaStatusUpdateStatus setValue(String value) {
27+
this.value = value;
28+
return this;
29+
}
30+
}

0 commit comments

Comments
 (0)