Skip to content

Commit f3449e9

Browse files
csvirimetacosm
andcommitted
feat: remove automatic observed generation handling (#2382)
Signed-off-by: Attila Mészáros <csviri@gmail.com> Signed-off-by: Chris Laprun <claprun@redhat.com> Co-authored-by: Chris Laprun <claprun@redhat.com> Signed-off-by: Attila Mészáros <csviri@gmail.com>
1 parent da82ec0 commit f3449e9

File tree

26 files changed

+164
-303
lines changed

26 files changed

+164
-303
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ It makes it easy to implement best practices and patterns for an Operator. Featu
4040
* Handling dependent resources, related events, and caching.
4141
* Automatic Retries
4242
* Smart event scheduling
43-
* Handling Observed Generations automatically
4443
* Easy to use Error Handling
4544
* ... and everything that a batteries included framework needs
4645

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package {{groupId}};
22

3-
import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus;
4-
5-
public class {{artifactClassId}}Status extends ObservedGenerationAwareStatus {
3+
public class {{artifactClassId}}Status {
64

75
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
package io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope;
22

3-
import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus;
4-
5-
public class BoundedCacheTestStatus extends ObservedGenerationAwareStatus {
3+
public class BoundedCacheTestStatus {
64
}

docs/documentation/features.md

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -186,57 +186,6 @@ annotation. If you do not specify a finalizer name, one will be automatically ge
186186

187187
From v5 by default finalizer is added using Served Side Apply. See also UpdateControl in docs.
188188

189-
## Automatic Observed Generation Handling
190-
191-
Having an `.observedGeneration` value on your resources' status is a best practice to
192-
indicate the last generation of the resource which was successfully reconciled by the controller.
193-
This helps users / administrators diagnose potential issues.
194-
195-
In order to have this feature working:
196-
197-
- the **status class** (not the resource itself) must implement the
198-
[`ObservedGenerationAware`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java)
199-
interface. See also
200-
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)
201-
convenience implementation that you can extend in your own status class implementations.
202-
- The other condition is that the `CustomResource.getStatus()` method should not return `null`.
203-
So the status should be instantiated when the object is returned using the `UpdateControl`.
204-
205-
If these conditions are fulfilled and generation awareness is activated, the observed generation
206-
is automatically set by the framework after the `reconcile` method is called.
207-
208-
When using SSA based patches, the observed generation is only updated when `UpdateControl.patchStatus` or
209-
`UpdateControl.patchResourceAndStatus` is returned. In case the of non-SSA based patches
210-
the observed generation is also updated even when `UpdateControl.noUpdate()` is returned from the
211-
reconciler.
212-
See this feature at work in the [WebPage example](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java#L5).
213-
See turning off an on the SSA based patching at [`ConfigurationServcice.useSSAToPatchPrimaryResource()`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L385-L385).
214-
215-
```java
216-
public class WebPageStatus extends ObservedGenerationAwareStatus {
217-
// omitted code
218-
}
219-
```
220-
221-
Initializing status automatically on custom resource could be done by overriding the `initStatus` method
222-
of `CustomResource`. However, this is NOT advised, since breaks the status patching if you use:
223-
`UpdateControl.patchStatus`. See
224-
also [javadocs](https://github.com/java-operator-sdk/java-operator-sdk/blob/3994f5ffc1fb000af81aa198abf72a5f75fd3e97/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java#L41-L42)
225-
.
226-
227-
```java
228-
@Group("sample.javaoperatorsdk")
229-
@Version("v1")
230-
public class WebPage extends CustomResource<WebPageSpec, WebPageStatus>
231-
implements Namespaced {
232-
233-
@Override
234-
protected WebPageStatus initStatus() {
235-
return new WebPageStatus();
236-
}
237-
}
238-
```
239-
240189
## Generation Awareness and Event Filtering
241190

242191
A best practice when an operator starts up is to reconcile all the associated resources because
@@ -254,9 +203,7 @@ To turn off this feature, set `generationAwareEventProcessing` to `false` for th
254203
## Support for Well Known (non-custom) Kubernetes Resources
255204

256205
A Controller can be registered for a non-custom resource, so well known Kubernetes resources like (
257-
`Ingress`, `Deployment`,...). Note that automatic observed generation handling is not supported
258-
for these resources, though, in this case, the handling of the observed generation is probably
259-
handled by the primary controller.
206+
`Ingress`, `Deployment`,...).
260207

261208
See
262209
the [integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java)

docs/documentation/v5-0-migration.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,6 @@ permalink: /docs/v5-0-migration
4444
where it is demonstrated. Also, the related part of
4545
a [workaround](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/StatusPatchSSAMigrationIT.java#L110-L116).
4646

47-
Related automatic observed generation handling changes:
48-
Automated Observed Generation (see features in docs), is automatically handled for non-SSA, even if
49-
the status sub-resource is not instructed to be updated. This is not true for SSA, observed generation is updated
50-
only when patch status is instructed by `UpdateControl`.
51-
5247
6. `ManagedDependentResourceContext` has been renamed to `ManagedWorkflowAndDependentResourceContext` and is accessed
5348
via the accordingly renamed `managedWorkflowAndDependentResourceContext` method.
5449
7. `ResourceDiscriminator` was removed. In most of the cases you can just delete the discriminator, everything should
@@ -57,3 +52,9 @@ permalink: /docs/v5-0-migration
5752
8. `ConfigurationService.getTerminationTimeoutSeconds` and associated overriding mechanism have been removed,
5853
use `Operator.stop(Duration)` instead.
5954
9. `Operator.installShutdownHook()` has been removed, use `Operator.installShutdownHook(Duration)` instead
55+
10. Automated observed generation handling feature was removed (`ObservedGenerationAware` interface
56+
and `ObservedGenerationAwareStatus` class were deleted). Manually handling observed generation is fairly easy to do
57+
in your reconciler, however, it cannot be done automatically when using SSA. We therefore removed the feature since
58+
it would have been confusing to have a different behavior for SSA and non-SSA cases. For an example of how to do
59+
observed generation handling manually in your reconciler, see
60+
[this sample](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/manualobservedgeneration/ManualObservedGenerationReconciler.java).

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

Lines changed: 0 additions & 29 deletions
This file was deleted.

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

Lines changed: 0 additions & 19 deletions
This file was deleted.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ default boolean parseResourceVersionsForEventFilteringAndCaching() {
395395
/**
396396
* {@link io.javaoperatorsdk.operator.api.reconciler.UpdateControl} patch resource or status can
397397
* either use simple patches or SSA. Setting this to {@code true}, controllers will use SSA for
398-
* adding finalizers, managing observed generation, patching resources and status.
398+
* adding finalizers, patching resources and status.
399399
*
400400
* @return {@code true} if Server-Side Apply (SSA) should be used when patching the primary
401401
* resources, {@code false} otherwise

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

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@
1010
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
1111
import io.fabric8.kubernetes.api.model.Namespaced;
1212
import io.fabric8.kubernetes.api.model.ObjectMeta;
13-
import io.fabric8.kubernetes.client.CustomResource;
1413
import io.fabric8.kubernetes.client.KubernetesClientException;
1514
import io.fabric8.kubernetes.client.dsl.MixedOperation;
1615
import io.fabric8.kubernetes.client.dsl.Resource;
1716
import io.fabric8.kubernetes.client.dsl.base.PatchContext;
1817
import io.fabric8.kubernetes.client.dsl.base.PatchType;
1918
import io.javaoperatorsdk.operator.OperatorException;
20-
import io.javaoperatorsdk.operator.api.ObservedGenerationAware;
2119
import io.javaoperatorsdk.operator.api.config.Cloner;
2220
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
2321
import io.javaoperatorsdk.operator.api.reconciler.BaseControl;
@@ -166,13 +164,8 @@ private PostExecutionControl<P> reconcileExecution(ExecutionScope<P> executionSc
166164
}
167165
}
168166

169-
// check if status also needs to be updated
170-
final var updateObservedGeneration = updateControl.isNoUpdate()
171-
? shouldUpdateObservedGenerationAutomatically(resourceForExecution)
172-
: shouldUpdateObservedGenerationAutomatically(updatedCustomResource);
173-
// if using SSA the observed generation is updated only if user instructs patching the status
174-
if (updateControl.isPatchStatus() || (updateObservedGeneration && !useSSA)) {
175-
updatedCustomResource = patchStatusGenerationAware(toUpdate, originalResource);
167+
if (updateControl.isPatchStatus()) {
168+
customResourceFacade.patchStatus(toUpdate, originalResource);
176169
}
177170
return createPostExecutionControl(updatedCustomResource, updateControl);
178171
}
@@ -202,9 +195,8 @@ public boolean isLastAttempt() {
202195

203196
P updatedResource = null;
204197
if (errorStatusUpdateControl.getResource().isPresent()) {
205-
updatedResource =
206-
patchStatusGenerationAware(errorStatusUpdateControl.getResource().orElseThrow(),
207-
originalResource);
198+
updatedResource = customResourceFacade
199+
.patchStatus(errorStatusUpdateControl.getResource().orElseThrow(), originalResource);
208200
}
209201
if (errorStatusUpdateControl.isNoRetry()) {
210202
PostExecutionControl<P> postExecutionControl;
@@ -230,38 +222,9 @@ private boolean isErrorStatusHandlerPresent() {
230222
}
231223

232224
private P patchStatusGenerationAware(P resource, P originalResource) {
233-
updateStatusObservedGenerationIfRequired(resource);
234225
return customResourceFacade.patchStatus(resource, originalResource);
235226
}
236227

237-
@SuppressWarnings("rawtypes")
238-
private boolean shouldUpdateObservedGenerationAutomatically(P resource) {
239-
if (configuration().isGenerationAware() && resource instanceof CustomResource<?, ?>) {
240-
var customResource = (CustomResource) resource;
241-
var status = customResource.getStatus();
242-
// Note that if status is null we won't update the observed generation.
243-
if (status instanceof ObservedGenerationAware) {
244-
var observedGen = ((ObservedGenerationAware) status).getObservedGeneration();
245-
var currentGen = resource.getMetadata().getGeneration();
246-
return !currentGen.equals(observedGen);
247-
}
248-
}
249-
return false;
250-
}
251-
252-
@SuppressWarnings("rawtypes")
253-
private void updateStatusObservedGenerationIfRequired(P resource) {
254-
if (configuration().isGenerationAware() && resource instanceof CustomResource<?, ?>) {
255-
var customResource = (CustomResource) resource;
256-
var status = customResource.getStatus();
257-
// Note that if status is null we won't update the observed generation.
258-
if (status instanceof ObservedGenerationAware) {
259-
((ObservedGenerationAware) status)
260-
.setObservedGeneration(resource.getMetadata().getGeneration());
261-
}
262-
}
263-
}
264-
265228
private PostExecutionControl<P> createPostExecutionControl(P updatedCustomResource,
266229
UpdateControl<P> updateControl) {
267230
PostExecutionControl<P> postExecutionControl;

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

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -435,29 +435,6 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() {
435435
.isEqualTo(1000L);
436436
}
437437

438-
@Test
439-
void setObservedGenerationForStatusIfNeeded() throws Exception {
440-
var observedGenResource = createObservedGenCustomResource();
441-
442-
Reconciler<ObservedGenCustomResource> reconciler = mock(Reconciler.class);
443-
ControllerConfiguration<ObservedGenCustomResource> config =
444-
MockControllerConfiguration.forResource(ObservedGenCustomResource.class);
445-
CustomResourceFacade<ObservedGenCustomResource> facade = mock(CustomResourceFacade.class);
446-
var dispatcher = init(observedGenResource, reconciler, config, facade, true);
447-
448-
when(config.isGenerationAware()).thenReturn(true);
449-
450-
when(reconciler.reconcile(any(), any()))
451-
.thenReturn(UpdateControl.patchStatus(observedGenResource));
452-
when(facade.patchStatus(eq(observedGenResource), any())).thenReturn(observedGenResource);
453-
454-
PostExecutionControl<ObservedGenCustomResource> control = dispatcher.handleExecution(
455-
executionScopeWithCREvent(observedGenResource));
456-
assertThat(control.getUpdatedCustomResource().orElseGet(() -> fail("Missing optional"))
457-
.getStatus().getObservedGeneration())
458-
.isEqualTo(1L);
459-
}
460-
461438
@Test
462439
void doesNotUpdatesObservedGenerationIfStatusIsNotPatchedWhenUsingSSA() throws Exception {
463440
var observedGenResource = createObservedGenCustomResource();
@@ -476,28 +453,6 @@ void doesNotUpdatesObservedGenerationIfStatusIsNotPatchedWhenUsingSSA() throws E
476453
assertThat(control.getUpdatedCustomResource()).isEmpty();
477454
}
478455

479-
@Test
480-
void patchObservedGenerationOnCustomResourcePatchIfNoSSA() throws Exception {
481-
var observedGenResource = createObservedGenCustomResource();
482-
483-
Reconciler<ObservedGenCustomResource> reconciler = mock(Reconciler.class);
484-
final var config = MockControllerConfiguration.forResource(ObservedGenCustomResource.class);
485-
CustomResourceFacade<ObservedGenCustomResource> facade = mock(CustomResourceFacade.class);
486-
when(config.isGenerationAware()).thenReturn(true);
487-
when(reconciler.reconcile(any(), any()))
488-
.thenReturn(UpdateControl.patchResource(observedGenResource));
489-
when(facade.patchResource(any(), any())).thenReturn(observedGenResource);
490-
when(facade.patchStatus(eq(observedGenResource), any())).thenReturn(observedGenResource);
491-
initConfigService(false);
492-
var dispatcher = init(observedGenResource, reconciler, config, facade, true);
493-
494-
PostExecutionControl<ObservedGenCustomResource> control = dispatcher.handleExecution(
495-
executionScopeWithCREvent(observedGenResource));
496-
assertThat(control.getUpdatedCustomResource().orElseGet(() -> fail("Missing optional"))
497-
.getStatus().getObservedGeneration())
498-
.isEqualTo(1L);
499-
}
500-
501456
@Test
502457
void doesNotPatchObservedGenerationOnCustomResourcePatch() throws Exception {
503458
var observedGenResource = createObservedGenCustomResource();
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package io.javaoperatorsdk.operator.sample.observedgeneration;
22

3-
import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus;
4-
5-
public class ObservedGenStatus extends ObservedGenerationAwareStatus {
3+
public class ObservedGenStatus {
64

75
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.extension.RegisterExtension;
5+
6+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
7+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
8+
import io.javaoperatorsdk.operator.sample.manualobservedgeneration.ManualObservedGenerationCustomResource;
9+
import io.javaoperatorsdk.operator.sample.manualobservedgeneration.ManualObservedGenerationReconciler;
10+
import io.javaoperatorsdk.operator.sample.manualobservedgeneration.ManualObservedGenerationSpec;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.awaitility.Awaitility.await;
14+
15+
public class ManualObservedGenerationIT {
16+
17+
public static final String RESOURCE_NAME = "test1";
18+
@RegisterExtension
19+
LocallyRunOperatorExtension extension =
20+
LocallyRunOperatorExtension.builder().withReconciler(new ManualObservedGenerationReconciler())
21+
.build();
22+
23+
@Test
24+
void observedGenerationUpdated() {
25+
extension.create(testResource());
26+
27+
await().untilAsserted(() -> {
28+
var r = extension.get(ManualObservedGenerationCustomResource.class, RESOURCE_NAME);
29+
assertThat(r).isNotNull();
30+
assertThat(r.getStatus().getObservedGeneration()).isEqualTo(1);
31+
assertThat(r.getStatus().getObservedGeneration()).isEqualTo(r.getMetadata().getGeneration());
32+
});
33+
34+
var changed = testResource();
35+
changed.getSpec().setValue("changed value");
36+
extension.replace(changed);
37+
38+
await().untilAsserted(() -> {
39+
var r = extension.get(ManualObservedGenerationCustomResource.class, RESOURCE_NAME);
40+
assertThat(r.getStatus().getObservedGeneration()).isEqualTo(2);
41+
assertThat(r.getStatus().getObservedGeneration()).isEqualTo(r.getMetadata().getGeneration());
42+
});
43+
}
44+
45+
ManualObservedGenerationCustomResource testResource() {
46+
var res = new ManualObservedGenerationCustomResource();
47+
res.setMetadata(new ObjectMetaBuilder()
48+
.withName(RESOURCE_NAME)
49+
.build());
50+
res.setSpec(new ManualObservedGenerationSpec());
51+
res.getSpec().setValue("Initial Value");
52+
return res;
53+
}
54+
}

0 commit comments

Comments
 (0)