Skip to content

Commit 3d1f4a0

Browse files
authored
ErrorStatusHandler - support for better error reporting in status (#685)
1 parent 33c2eff commit 3d1f4a0

File tree

12 files changed

+323
-56
lines changed

12 files changed

+323
-56
lines changed

Diff for: docs/documentation/features.md

+24
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,30 @@ won't be a result of a retry, but the new event.
127127

128128
A successful execution resets the retry.
129129

130+
### Setting Error Status After Last Retry Attempt
131+
132+
In order to facilitate error reporting in case a last retry attempt fails, Reconciler can implement the following
133+
interface:
134+
135+
```java
136+
public interface ErrorStatusHandler<T extends CustomResource<?, ?>> {
137+
138+
T updateErrorStatus(T resource, RuntimeException e);
139+
140+
}
141+
```
142+
143+
The `updateErrorStatus` resource is called when it's the last retry attempt according the retry configuration and the
144+
reconciler execution still resulted in a runtime exception.
145+
146+
The result of the method call is used to make a status sub-resource update on the custom resource. This is always a
147+
sub-resource update request, so no update on custom resource itself (like spec of metadata) happens. Note that this
148+
update request will also produce an event, and will result in a reconciliation if the controller is not generation
149+
aware.
150+
151+
Note that the scope of this feature is only the `reconcile` method of the reconciler, since there should not be updates
152+
on custom resource after it is marked for deletion.
153+
130154
### Correctness and Automatic Retries
131155

132156
There is a possibility to turn of the automatic retries. This is not desirable, unless there is a very specific reason.

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44

55
public interface Cloner {
66

7-
CustomResource<?, ?> clone(CustomResource<?, ?> object);
7+
<T extends CustomResource<?, ?>> T clone(T object);
88

99
}

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ public interface ConfigurationService {
1919
private final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
2020

2121
@Override
22-
public CustomResource<?, ?> clone(CustomResource<?, ?> object) {
22+
public <T extends CustomResource<?, ?>> T clone(T object) {
2323
try {
24-
return OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsString(object), object.getClass());
24+
return OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsString(object),
25+
(Class<T>) object.getClass());
2526
} catch (JsonProcessingException e) {
2627
throw new IllegalStateException(e);
2728
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.javaoperatorsdk.operator.api.reconciler;
2+
3+
import io.fabric8.kubernetes.client.CustomResource;
4+
5+
public interface ErrorStatusHandler<T extends CustomResource<?, ?>> {
6+
7+
/**
8+
* <p>
9+
* Reconcile can implement this interface in order to update the status sub-resource in the case
10+
* when the last reconciliation retry attempt is failed on the Reconciler. In that case the
11+
* updateErrorStatus is called automatically.
12+
* <p>
13+
* The result of the method call is used to make a status sub-resource update on the custom
14+
* resource. This is always a sub-resource update request, so no update on custom resource itself
15+
* (like spec of metadata) happens. Note that this update request will also produce an event, and
16+
* will result in a reconciliation if the controller is not generation aware.
17+
* <p>
18+
* Note that the scope of this feature is only the reconcile method of the reconciler, since there
19+
* should not be updates on custom resource after it is marked for deletion.
20+
*
21+
* @param resource to update the status on
22+
* @param e exception thrown from the reconciler
23+
* @return the updated resource
24+
*/
25+
T updateErrorStatus(T resource, RuntimeException e);
26+
27+
}

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java

+69-19
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.javaoperatorsdk.operator.api.reconciler.Context;
1515
import io.javaoperatorsdk.operator.api.reconciler.DefaultContext;
1616
import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
17+
import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler;
1718
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
1819
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
1920

@@ -109,27 +110,76 @@ private PostExecutionControl<R> handleCreateOrUpdate(
109110
updateCustomResourceWithFinalizer(resource);
110111
return PostExecutionControl.onlyFinalizerAdded();
111112
} else {
112-
log.debug(
113-
"Executing createOrUpdate for resource {} with version: {} with execution scope: {}",
114-
getName(resource),
115-
getVersion(resource),
116-
executionScope);
113+
try {
114+
var resourceForExecution =
115+
cloneResourceForErrorStatusHandlerIfNeeded(resource, context);
116+
return createOrUpdateExecution(executionScope, resourceForExecution, context);
117+
} catch (RuntimeException e) {
118+
handleLastAttemptErrorStatusHandler(resource, context, e);
119+
throw e;
120+
}
121+
}
122+
}
123+
124+
/**
125+
* Resource make sense only to clone for the ErrorStatusHandler. Otherwise, this operation can be
126+
* skipped since it can be memory and time-consuming. However, it needs to be cloned since it's
127+
* common that the custom resource is changed during an execution, and it's much cleaner to have
128+
* to original resource in place for status update.
129+
*/
130+
private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) {
131+
if (isLastAttemptOfRetryAndErrorStatusHandlerPresent(context)) {
132+
return controller.getConfiguration().getConfigurationService().getResourceCloner()
133+
.clone(resource);
134+
} else {
135+
return resource;
136+
}
137+
}
117138

118-
UpdateControl<R> updateControl = controller.reconcile(resource, context);
119-
R updatedCustomResource = null;
120-
if (updateControl.isUpdateCustomResourceAndStatusSubResource()) {
121-
updatedCustomResource = updateCustomResource(updateControl.getCustomResource());
122-
updateControl
123-
.getCustomResource()
124-
.getMetadata()
125-
.setResourceVersion(updatedCustomResource.getMetadata().getResourceVersion());
126-
updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource());
127-
} else if (updateControl.isUpdateStatusSubResource()) {
128-
updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource());
129-
} else if (updateControl.isUpdateCustomResource()) {
130-
updatedCustomResource = updateCustomResource(updateControl.getCustomResource());
139+
private PostExecutionControl<R> createOrUpdateExecution(ExecutionScope<R> executionScope,
140+
R resource, Context context) {
141+
log.debug(
142+
"Executing createOrUpdate for resource {} with version: {} with execution scope: {}",
143+
getName(resource),
144+
getVersion(resource),
145+
executionScope);
146+
147+
UpdateControl<R> updateControl = controller.reconcile(resource, context);
148+
R updatedCustomResource = null;
149+
if (updateControl.isUpdateCustomResourceAndStatusSubResource()) {
150+
updatedCustomResource = updateCustomResource(updateControl.getCustomResource());
151+
updateControl
152+
.getCustomResource()
153+
.getMetadata()
154+
.setResourceVersion(updatedCustomResource.getMetadata().getResourceVersion());
155+
updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource());
156+
} else if (updateControl.isUpdateStatusSubResource()) {
157+
updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource());
158+
} else if (updateControl.isUpdateCustomResource()) {
159+
updatedCustomResource = updateCustomResource(updateControl.getCustomResource());
160+
}
161+
return createPostExecutionControl(updatedCustomResource, updateControl);
162+
}
163+
164+
private void handleLastAttemptErrorStatusHandler(R resource, Context context,
165+
RuntimeException e) {
166+
if (isLastAttemptOfRetryAndErrorStatusHandlerPresent(context)) {
167+
try {
168+
var updatedResource = ((ErrorStatusHandler<R>) controller.getReconciler())
169+
.updateErrorStatus(resource, e);
170+
customResourceFacade.updateStatus(updatedResource);
171+
} catch (RuntimeException ex) {
172+
log.error("Error during error status handling.", ex);
131173
}
132-
return createPostExecutionControl(updatedCustomResource, updateControl);
174+
}
175+
}
176+
177+
private boolean isLastAttemptOfRetryAndErrorStatusHandlerPresent(Context context) {
178+
if (context.getRetryInfo().isPresent()) {
179+
return context.getRetryInfo().get().isLastAttempt()
180+
&& controller.getReconciler() instanceof ErrorStatusHandler;
181+
} else {
182+
return false;
133183
}
134184
}
135185

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public Optional<T> getCustomResource(CustomResourceID resourceID) {
172172
if (resource == null) {
173173
return Optional.empty();
174174
} else {
175-
return Optional.of((T) (cloner.clone(resource)));
175+
return Optional.of(cloner.clone(resource));
176176
}
177177
}
178178

0 commit comments

Comments
 (0)