Skip to content

Commit 409cb1d

Browse files
authored
fix: null values deleted - using edit (#1219)
1 parent 9da4315 commit 409cb1d

18 files changed

+127
-69
lines changed

docs/documentation/features.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,8 @@ In order to have this feature working:
152152
interface. See also
153153
the [`ObservedGenerationAwareStatus`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java)
154154
which can also be extended.
155-
- The other condition is that the `CustomResource.getStatus()` method should not return `null`
156-
, but an instance of the class representing `status`. The best way to achieve this is to
157-
override [`CustomResource.initStatus()`](https://github.com/fabric8io/kubernetes-client/blob/865e0ddf67b99f954aa55ab14e5806d53ae149ec/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/CustomResource.java#L139)
158-
.
155+
- The other condition is that the `CustomResource.getStatus()` should not return `null`. So the status should be instantiated
156+
when the object is returned using the `UpdateControl`.
159157

160158
If these conditions are fulfilled and generation awareness not turned off, the observed generation is automatically set
161159
by the framework after the `reconcile` method is called. Note that the observed generation is updated also

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.javaoperatorsdk.operator.api.reconciler;
22

33
import io.fabric8.kubernetes.api.model.HasMetadata;
4+
import io.fabric8.kubernetes.client.CustomResource;
45

56
public class UpdateControl<T extends HasMetadata> extends BaseControl<UpdateControl<T>> {
67

@@ -34,7 +35,15 @@ public static <T extends HasMetadata> UpdateControl<T> updateResource(T customRe
3435
}
3536

3637
/**
37-
* Preferred way to update the status. It does not do optimistic locking.
38+
* Preferred way to update the status. It does not do optimistic locking. Uses JSON Patch to patch
39+
* the resource.
40+
* <p>
41+
* Note that this does not work, if the {@link CustomResource#initStatus() initStatus} is
42+
* implemented, since it breaks the diffing process. Don't implement it if using this method.
43+
* </p>
44+
* There is also an issue with setting value to null with older Kubernetes versions (1.19 and
45+
* below). See: <a href=
46+
* "https://github.com/fabric8io/kubernetes-client/issues/4158">https://github.com/fabric8io/kubernetes-client/issues/4158</a>
3847
*
3948
* @param <T> resource type
4049
* @param customResource the custom resource with target status

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package io.javaoperatorsdk.operator.processing.event;
22

3+
import java.io.ByteArrayInputStream;
4+
import java.io.IOException;
5+
import java.nio.charset.StandardCharsets;
6+
37
import org.slf4j.Logger;
48
import org.slf4j.LoggerFactory;
59

@@ -9,6 +13,7 @@
913
import io.fabric8.kubernetes.client.dsl.MixedOperation;
1014
import io.fabric8.kubernetes.client.dsl.Resource;
1115
import io.fabric8.kubernetes.client.dsl.internal.HasMetadataOperationsImpl;
16+
import io.fabric8.kubernetes.client.utils.Serialization;
1217
import io.javaoperatorsdk.operator.api.ObservedGenerationAware;
1318
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider;
1419
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
@@ -96,31 +101,19 @@ private PostExecutionControl<R> handleReconcile(
96101
updateCustomResourceWithFinalizer(originalResource);
97102
return PostExecutionControl.onlyFinalizerAdded();
98103
} else {
104+
var resourceForExecution =
105+
cloneResource(originalResource);
99106
try {
100-
var resourceForExecution =
101-
cloneResourceForErrorStatusHandlerIfNeeded(originalResource);
102107
return reconcileExecution(executionScope, resourceForExecution, originalResource, context);
103108
} catch (Exception e) {
104-
return handleErrorStatusHandler(originalResource, context, e);
109+
return handleErrorStatusHandler(resourceForExecution, originalResource, context, e);
105110
}
106111
}
107112
}
108113

109-
/**
110-
* Resource make sense only to clone for the ErrorStatusHandler or if the observed generation in
111-
* status is handled automatically. Otherwise, this operation can be skipped since it can be
112-
* memory and time-consuming. However, it needs to be cloned since it's common that the custom
113-
* resource is changed during an execution, and it's much cleaner to have to original resource in
114-
* place for status update.
115-
*/
116-
private R cloneResourceForErrorStatusHandlerIfNeeded(R resource) {
117-
if (isErrorStatusHandlerPresent() ||
118-
shouldUpdateObservedGenerationAutomatically(resource)) {
119-
final var cloner = ConfigurationServiceProvider.instance().getResourceCloner();
120-
return cloner.clone(resource);
121-
} else {
122-
return resource;
123-
}
114+
private R cloneResource(R resource) {
115+
final var cloner = ConfigurationServiceProvider.instance().getResourceCloner();
116+
return cloner.clone(resource);
124117
}
125118

126119
private PostExecutionControl<R> reconcileExecution(ExecutionScope<R> executionScope,
@@ -141,27 +134,31 @@ private PostExecutionControl<R> reconcileExecution(ExecutionScope<R> executionSc
141134
.getMetadata()
142135
.setResourceVersion(updatedCustomResource.getMetadata().getResourceVersion());
143136
updatedCustomResource =
144-
updateStatusGenerationAware(updateControl.getResource(), updateControl.isPatch());
137+
updateStatusGenerationAware(updateControl.getResource(), originalResource,
138+
updateControl.isPatch());
145139
} else if (updateControl.isUpdateStatus()) {
146140
updatedCustomResource =
147-
updateStatusGenerationAware(updateControl.getResource(), updateControl.isPatch());
141+
updateStatusGenerationAware(updateControl.getResource(), originalResource,
142+
updateControl.isPatch());
148143
} else if (updateControl.isUpdateResource()) {
149144
updatedCustomResource =
150145
updateCustomResource(updateControl.getResource());
151146
if (shouldUpdateObservedGenerationAutomatically(updatedCustomResource)) {
152147
updatedCustomResource =
153-
updateStatusGenerationAware(originalResource, updateControl.isPatch());
148+
updateStatusGenerationAware(updateControl.getResource(), originalResource,
149+
updateControl.isPatch());
154150
}
155151
} else if (updateControl.isNoUpdate()
156152
&& shouldUpdateObservedGenerationAutomatically(resourceForExecution)) {
157153
updatedCustomResource =
158-
updateStatusGenerationAware(originalResource, updateControl.isPatch());
154+
updateStatusGenerationAware(originalResource, originalResource, updateControl.isPatch());
159155
}
160156
return createPostExecutionControl(updatedCustomResource, updateControl);
161157
}
162158

163159
@SuppressWarnings("unchecked")
164-
private PostExecutionControl<R> handleErrorStatusHandler(R resource, Context<R> context,
160+
private PostExecutionControl<R> handleErrorStatusHandler(R resource, R originalResource,
161+
Context<R> context,
165162
Exception e) throws Exception {
166163
if (isErrorStatusHandlerPresent()) {
167164
try {
@@ -183,7 +180,7 @@ public boolean isLastAttempt() {
183180
R updatedResource = null;
184181
if (errorStatusUpdateControl.getResource().isPresent()) {
185182
updatedResource = errorStatusUpdateControl.isPatch() ? customResourceFacade
186-
.patchStatus(errorStatusUpdateControl.getResource().orElseThrow())
183+
.patchStatus(errorStatusUpdateControl.getResource().orElseThrow(), originalResource)
187184
: customResourceFacade
188185
.updateStatus(errorStatusUpdateControl.getResource().orElseThrow());
189186
}
@@ -207,10 +204,10 @@ private boolean isErrorStatusHandlerPresent() {
207204
return controller.getReconciler() instanceof ErrorStatusHandler;
208205
}
209206

210-
private R updateStatusGenerationAware(R resource, boolean patch) {
207+
private R updateStatusGenerationAware(R resource, R originalResource, boolean patch) {
211208
updateStatusObservedGenerationIfRequired(resource);
212209
if (patch) {
213-
return customResourceFacade.patchStatus(resource);
210+
return customResourceFacade.patchStatus(resource, originalResource);
214211
} else {
215212
return customResourceFacade.updateStatus(resource);
216213
}
@@ -357,18 +354,24 @@ public R updateStatus(R resource) {
357354
return (R) hasMetadataOperation.replaceStatus(resource);
358355
}
359356

360-
public R patchStatus(R resource) {
357+
public R patchStatus(R resource, R originalResource) {
361358
log.trace("Updating status for resource: {}", resource);
362359
String resourceVersion = resource.getMetadata().getResourceVersion();
363-
try {
364-
// don't do optimistic locking on patch
365-
resource.getMetadata().setResourceVersion(null);
360+
// don't do optimistic locking on patch
361+
originalResource.getMetadata().setResourceVersion(null);
362+
resource.getMetadata().setResourceVersion(null);
363+
try (var bis = new ByteArrayInputStream(
364+
Serialization.asJson(originalResource).getBytes(StandardCharsets.UTF_8))) {
366365
return resourceOperation
367366
.inNamespace(resource.getMetadata().getNamespace())
368-
.withName(getName(resource))
369-
.patchStatus(resource);
367+
// will be simplified in fabric8 v6
368+
.load(bis)
369+
.editStatus(r -> resource);
370+
} catch (IOException e) {
371+
throw new IllegalStateException(e);
370372
} finally {
371373
// restore initial resource version
374+
originalResource.getMetadata().setResourceVersion(resourceVersion);
372375
resource.getMetadata().setResourceVersion(resourceVersion);
373376
}
374377
}

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() {
142142

143143
reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource));
144144

145-
verify(customResourceFacade, times(1)).patchStatus(testCustomResource);
145+
verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any());
146146
verify(customResourceFacade, never()).replaceResourceWithLock(any());
147147
}
148148

@@ -168,7 +168,7 @@ void patchesStatus() {
168168

169169
reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource));
170170

171-
verify(customResourceFacade, times(1)).patchStatus(testCustomResource);
171+
verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any());
172172
verify(customResourceFacade, never()).updateStatus(any());
173173
verify(customResourceFacade, never()).replaceResourceWithLock(any());
174174
}
@@ -362,7 +362,7 @@ void setObservedGenerationForStatusIfNeeded() throws Exception {
362362

363363
when(reconciler.reconcile(any(), any()))
364364
.thenReturn(UpdateControl.patchStatus(observedGenResource));
365-
when(facade.patchStatus(observedGenResource)).thenReturn(observedGenResource);
365+
when(facade.patchStatus(eq(observedGenResource), any())).thenReturn(observedGenResource);
366366

367367
PostExecutionControl<ObservedGenCustomResource> control = dispatcher.handleExecution(
368368
executionScopeWithCREvent(observedGenResource));
@@ -521,7 +521,7 @@ void errorStatusHandlerCanPatchResource() {
521521
new ExecutionScope(
522522
testCustomResource, null));
523523

524-
verify(customResourceFacade, times(1)).patchStatus(testCustomResource);
524+
verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any());
525525
verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource),
526526
any(), any());
527527
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,8 @@ void invalidEventsDoesNotBreakEventHandling() {
6565
assertThat(
6666
operator
6767
.get(MultiVersionCRDTestCustomResource2.class, CR_V2_NAME)
68-
.getStatus()
69-
.getReconciledBy()
70-
.size())
71-
.isZero();
68+
.getStatus())
69+
.isNull();
7270
}
7371

7472

operator-framework/src/test/java/io/javaoperatorsdk/operator/StatusPatchNotLockingIT.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
import java.time.Duration;
44
import java.util.Map;
55

6+
import org.junit.jupiter.api.Disabled;
67
import org.junit.jupiter.api.Test;
78
import org.junit.jupiter.api.extension.RegisterExtension;
89

910
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
1011
import io.javaoperatorsdk.operator.junit.LocalOperatorExtension;
1112
import io.javaoperatorsdk.operator.sample.statuspatchnonlocking.StatusPatchLockingCustomResource;
13+
import io.javaoperatorsdk.operator.sample.statuspatchnonlocking.StatusPatchLockingCustomResourceSpec;
1214
import io.javaoperatorsdk.operator.sample.statuspatchnonlocking.StatusPatchLockingReconciler;
1315

16+
import static io.javaoperatorsdk.operator.sample.statuspatchnonlocking.StatusPatchLockingReconciler.MESSAGE;
1417
import static io.javaoperatorsdk.operator.sample.statusupdatelocking.StatusUpdateLockingReconciler.WAIT_TIME;
1518
import static org.assertj.core.api.Assertions.assertThat;
1619
import static org.awaitility.Awaitility.await;
@@ -44,8 +47,34 @@ void noOptimisticLockingDoneOnStatusUpdate() throws InterruptedException {
4447
});
4548
}
4649

50+
// see https://github.com/fabric8io/kubernetes-client/issues/4158
51+
@Disabled("Patch generated supported by K8S version below 1.20.x")
52+
@Test
53+
void valuesAreDeletedIfSetToNull() {
54+
var resource = operator.create(StatusPatchLockingCustomResource.class, createResource());
55+
56+
await().untilAsserted(() -> {
57+
var actual = operator.get(StatusPatchLockingCustomResource.class,
58+
TEST_RESOURCE_NAME);
59+
assertThat(actual.getStatus()).isNotNull();
60+
assertThat(actual.getStatus().getMessage()).isEqualTo(MESSAGE);
61+
});
62+
63+
resource.getSpec().setMessageInStatus(false);
64+
operator.replace(StatusPatchLockingCustomResource.class, resource);
65+
66+
await().untilAsserted(() -> {
67+
var actual = operator.get(StatusPatchLockingCustomResource.class,
68+
TEST_RESOURCE_NAME);
69+
assertThat(actual.getStatus()).isNotNull();
70+
assertThat(actual.getStatus().getMessage()).isNull();
71+
});
72+
}
73+
74+
4775
StatusPatchLockingCustomResource createResource() {
4876
StatusPatchLockingCustomResource res = new StatusPatchLockingCustomResource();
77+
res.setSpec(new StatusPatchLockingCustomResourceSpec());
4978
res.setMetadata(new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).build());
5079
return res;
5180
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/changenamespace/ChangeNamespaceTestCustomResource.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,4 @@ public class ChangeNamespaceTestCustomResource
1111
extends CustomResource<Void, ChangeNamespaceTestCustomResourceStatus>
1212
implements Namespaced {
1313

14-
@Override
15-
protected ChangeNamespaceTestCustomResourceStatus initStatus() {
16-
return new ChangeNamespaceTestCustomResourceStatus();
17-
}
1814
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/changenamespace/ChangeNamespaceTestReconciler.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ public UpdateControl<ChangeNamespaceTestCustomResource> reconcile(
4545
}
4646

4747
increaseNumberOfResourceExecutions(primary);
48-
48+
if (primary.getStatus() == null) {
49+
primary.setStatus(new ChangeNamespaceTestCustomResourceStatus());
50+
}
4951
var statusUpdates = primary.getStatus().getNumberOfStatusUpdates();
5052
primary.getStatus().setNumberOfStatusUpdates(statusUpdates + 1);
5153
return UpdateControl.patchStatus(primary);

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResource1.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,5 @@ public class MultiVersionCRDTestCustomResource1
1616
CustomResource<MultiVersionCRDTestCustomResourceSpec1, MultiVersionCRDTestCustomResourceStatus1>
1717
implements Namespaced {
1818

19-
@Override
20-
protected MultiVersionCRDTestCustomResourceStatus1 initStatus() {
21-
return new MultiVersionCRDTestCustomResourceStatus1();
22-
}
2319

2420
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResource2.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,4 @@ public class MultiVersionCRDTestCustomResource2
1616
CustomResource<MultiVersionCRDTestCustomResourceSpec2, MultiVersionCRDTestCustomResourceStatus2>
1717
implements Namespaced {
1818

19-
@Override
20-
protected MultiVersionCRDTestCustomResourceStatus2 initStatus() {
21-
return new MultiVersionCRDTestCustomResourceStatus2();
22-
}
23-
2419
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public UpdateControl<MultiVersionCRDTestCustomResource1> reconcile(
2020
Context<MultiVersionCRDTestCustomResource1> context) {
2121
log.info("Reconcile MultiVersionCRDTestCustomResource1: {}",
2222
resource.getMetadata().getName());
23+
if (resource.getStatus() == null) {
24+
resource.setStatus(new MultiVersionCRDTestCustomResourceStatus1());
25+
}
2326
resource.getStatus().setValue1(resource.getStatus().getValue1() + 1);
2427
if (!resource.getStatus().getReconciledBy().contains(getClass().getSimpleName())) {
2528
resource.getStatus().getReconciledBy().add(getClass().getSimpleName());

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public UpdateControl<MultiVersionCRDTestCustomResource2> reconcile(
2020
Context<MultiVersionCRDTestCustomResource2> context) {
2121
log.info("Reconcile MultiVersionCRDTestCustomResource2: {}",
2222
resource.getMetadata().getName());
23+
if (resource.getStatus() == null) {
24+
resource.setStatus(new MultiVersionCRDTestCustomResourceStatus2());
25+
}
2326
resource.getStatus().setValue1(resource.getStatus().getValue1() + 1);
2427
if (!resource.getStatus().getReconciledBy().contains(getClass().getSimpleName())) {
2528
resource.getStatus().getReconciledBy().add(getClass().getSimpleName());

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResource.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,4 @@ public class ObservedGenerationTestCustomResource
1515
extends CustomResource<Void, ObservedGenerationTestCustomResourceStatus>
1616
implements Namespaced {
1717

18-
@Override
19-
protected ObservedGenerationTestCustomResourceStatus initStatus() {
20-
return new ObservedGenerationTestCustomResourceStatus();
21-
}
22-
2318
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public UpdateControl<ObservedGenerationTestCustomResource> reconcile(
2020
Context<ObservedGenerationTestCustomResource> context) {
2121
log.info("Reconcile ObservedGenerationTestCustomResource: {}",
2222
resource.getMetadata().getName());
23+
if (resource.getStatus() == null) {
24+
resource.setStatus(new ObservedGenerationTestCustomResourceStatus());
25+
}
2326
return UpdateControl.patchStatus(resource);
2427
}
2528
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/statuspatchnonlocking/StatusPatchLockingCustomResource.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@
1212
@Kind("StatusUpdateLockingCustomResource")
1313
@ShortNames("sul")
1414
public class StatusPatchLockingCustomResource
15-
extends CustomResource<Void, StatusPatchLockingCustomResourceStatus>
15+
extends
16+
CustomResource<StatusPatchLockingCustomResourceSpec, StatusPatchLockingCustomResourceStatus>
1617
implements Namespaced {
1718

18-
@Override
19-
protected StatusPatchLockingCustomResourceStatus initStatus() {
20-
return new StatusPatchLockingCustomResourceStatus();
21-
}
2219
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.javaoperatorsdk.operator.sample.statuspatchnonlocking;
2+
3+
public class StatusPatchLockingCustomResourceSpec {
4+
5+
private boolean messageInStatus = true;
6+
7+
public boolean isMessageInStatus() {
8+
return messageInStatus;
9+
}
10+
11+
public StatusPatchLockingCustomResourceSpec setMessageInStatus(boolean messageInStatus) {
12+
this.messageInStatus = messageInStatus;
13+
return this;
14+
}
15+
}

0 commit comments

Comments
 (0)