Skip to content

Commit 26cfdee

Browse files
authored
feat: Observed generation in status support (#628)
1 parent c5c83af commit 26cfdee

File tree

9 files changed

+218
-11
lines changed

9 files changed

+218
-11
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.javaoperatorsdk.operator.api;
2+
3+
import java.util.Optional;
4+
5+
import io.fabric8.kubernetes.client.CustomResource;
6+
7+
/**
8+
* If the custom resource's status implements this interface, the observed generation will be
9+
* automatically handled. The last observed generation will be updated on status when the status is
10+
* instructed to be updated (see below). In addition to that, controller configuration will be
11+
* checked if is set to generation aware. If generation aware config is turned off, this interface
12+
* is ignored.
13+
*
14+
* In order to work the status object returned by CustomResource.getStatus() should not be null. In
15+
* addition to that from the controller that the
16+
* {@link UpdateControl#updateStatusSubResource(CustomResource)} or
17+
* {@link UpdateControl#updateCustomResourceAndStatus(CustomResource)} should be returned. The
18+
* observed generation is not updated in other cases.
19+
*
20+
* @see ObservedGenerationAwareStatus
21+
*/
22+
public interface ObservedGenerationAware {
23+
24+
void setObservedGeneration(Long generation);
25+
26+
Optional<Long> getObservedGeneration();
27+
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.javaoperatorsdk.operator.api;
2+
3+
import java.util.Optional;
4+
5+
/**
6+
* A helper base class for status sub-resources classes to extend to support generate awareness.
7+
*/
8+
public class ObservedGenerationAwareStatus implements ObservedGenerationAware {
9+
10+
private Long observedGeneration;
11+
12+
@Override
13+
public void setObservedGeneration(Long generation) {
14+
this.observedGeneration = generation;
15+
}
16+
17+
@Override
18+
public Optional<Long> getObservedGeneration() {
19+
return Optional.ofNullable(observedGeneration);
20+
}
21+
}

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,18 +117,32 @@ private PostExecutionControl<R> handleCreateOrUpdate(
117117
.getCustomResource()
118118
.getMetadata()
119119
.setResourceVersion(updatedCustomResource.getMetadata().getResourceVersion());
120-
updatedCustomResource =
121-
customResourceFacade.updateStatus(updateControl.getCustomResource());
120+
updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource());
122121
} else if (updateControl.isUpdateStatusSubResource()) {
123-
updatedCustomResource =
124-
customResourceFacade.updateStatus(updateControl.getCustomResource());
122+
updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource());
125123
} else if (updateControl.isUpdateCustomResource()) {
126124
updatedCustomResource = updateCustomResource(updateControl.getCustomResource());
127125
}
128126
return createPostExecutionControl(updatedCustomResource, updateControl);
129127
}
130128
}
131129

130+
private R updateStatusGenerationAware(R customResource) {
131+
updateStatusObservedGenerationIfRequired(customResource);
132+
return customResourceFacade.updateStatus(customResource);
133+
}
134+
135+
private void updateStatusObservedGenerationIfRequired(R customResource) {
136+
if (controller.getConfiguration().isGenerationAware()) {
137+
var status = customResource.getStatus();
138+
// Note that if status is null we won't update the observed generation.
139+
if (status instanceof ObservedGenerationAware) {
140+
((ObservedGenerationAware) status)
141+
.setObservedGeneration(customResource.getMetadata().getGeneration());
142+
}
143+
}
144+
}
145+
132146
private PostExecutionControl<R> createPostExecutionControl(R updatedCustomResource,
133147
UpdateControl<R> updateControl) {
134148
PostExecutionControl<R> postExecutionControl;

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.javaoperatorsdk.operator.processing.event.internal;
22

33
import io.fabric8.kubernetes.client.CustomResource;
4+
import io.javaoperatorsdk.operator.api.ObservedGenerationAware;
45

56
/**
67
* Convenience implementations of, and utility methods for, {@link CustomResourceEventFilter}.
@@ -21,9 +22,18 @@ public final class CustomResourceEventFilters {
2122
};
2223

2324
private static final CustomResourceEventFilter<CustomResource> GENERATION_AWARE =
24-
(configuration, oldResource, newResource) -> oldResource == null
25-
|| !configuration.isGenerationAware()
26-
|| oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration();
25+
(configuration, oldResource, newResource) -> {
26+
final var status = newResource.getStatus();
27+
final var generationAware = configuration.isGenerationAware();
28+
if (generationAware && status instanceof ObservedGenerationAware) {
29+
var actualGeneration = newResource.getMetadata().getGeneration();
30+
var observedGeneration = ((ObservedGenerationAware) status)
31+
.getObservedGeneration();
32+
return observedGeneration.map(aLong -> actualGeneration > aLong).orElse(true);
33+
}
34+
return oldResource == null || !generationAware ||
35+
oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration();
36+
};
2737

2838
private static final CustomResourceEventFilter<CustomResource> PASSTHROUGH =
2939
(configuration, oldResource, newResource) -> true;

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.mockito.ArgumentCaptor;
1010
import org.mockito.ArgumentMatchers;
1111

12+
import io.fabric8.kubernetes.api.model.ObjectMeta;
1213
import io.fabric8.kubernetes.client.CustomResource;
1314
import io.javaoperatorsdk.operator.TestUtils;
1415
import io.javaoperatorsdk.operator.api.Context;
@@ -23,6 +24,7 @@
2324
import io.javaoperatorsdk.operator.processing.event.Event;
2425
import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent;
2526
import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction;
27+
import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource;
2628

2729
import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.ADDED;
2830
import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.UPDATED;
@@ -320,6 +322,33 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() {
320322
assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L);
321323
}
322324

325+
@Test
326+
void setObservedGenerationForStatusIfNeeded() {
327+
var observedGenResource = createObservedGenCustomResource();
328+
329+
when(configuration.isGenerationAware()).thenReturn(true);
330+
when(controller.createOrUpdateResource(eq(observedGenResource), any()))
331+
.thenReturn(
332+
UpdateControl.updateStatusSubResource(observedGenResource));
333+
334+
when(customResourceFacade.updateStatus(observedGenResource)).thenReturn(observedGenResource);
335+
336+
PostExecutionControl<ObservedGenCustomResource> control = eventDispatcher.handleExecution(
337+
executionScopeWithCREvent(ADDED, observedGenResource));
338+
assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration().get())
339+
.isEqualTo(1L);
340+
}
341+
342+
@Test
343+
private ObservedGenCustomResource createObservedGenCustomResource() {
344+
ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource();
345+
observedGenCustomResource.setMetadata(new ObjectMeta());
346+
observedGenCustomResource.getMetadata().setGeneration(1L);
347+
observedGenCustomResource.getMetadata().setFinalizers(new ArrayList<>());
348+
observedGenCustomResource.getMetadata().getFinalizers().add(DEFAULT_FINALIZER);
349+
return observedGenCustomResource;
350+
}
351+
323352
private void markForDeletion(CustomResource customResource) {
324353
customResource.getMetadata().setDeletionTimestamp("2019-8-10");
325354
}

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

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import org.junit.jupiter.api.Test;
88

99
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
10+
import io.fabric8.kubernetes.api.model.ObjectMeta;
11+
import io.fabric8.kubernetes.client.CustomResource;
1012
import io.fabric8.kubernetes.client.dsl.MixedOperation;
1113
import io.fabric8.kubernetes.client.dsl.Resource;
1214
import io.javaoperatorsdk.operator.TestUtils;
@@ -15,6 +17,7 @@
1517
import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration;
1618
import io.javaoperatorsdk.operator.processing.ConfiguredController;
1719
import io.javaoperatorsdk.operator.processing.event.EventHandler;
20+
import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource;
1821
import io.javaoperatorsdk.operator.sample.simple.TestCustomResource;
1922

2023
import static org.mockito.Mockito.any;
@@ -94,6 +97,31 @@ public void eventFilteredByCustomPredicateAndGenerationAware() {
9497
verify(eventHandler, times(1)).handleEvent(any());
9598
}
9699

100+
@Test
101+
public void observedGenerationFiltering() {
102+
var config = new ObservedGenControllerConfig(FINALIZER, true, null);
103+
when(config.getConfigurationService().getResourceCloner())
104+
.thenReturn(ConfigurationService.DEFAULT_CLONER);
105+
106+
var controller = new ObservedGenConfiguredController(config);
107+
var eventSource = new CustomResourceEventSource<>(controller);
108+
eventSource.setEventHandler(eventHandler);
109+
110+
ObservedGenCustomResource cr = new ObservedGenCustomResource();
111+
cr.setMetadata(new ObjectMeta());
112+
cr.getMetadata().setFinalizers(List.of(FINALIZER));
113+
cr.getMetadata().setGeneration(5L);
114+
cr.getStatus().setObservedGeneration(5L);
115+
116+
eventSource.eventReceived(ResourceAction.UPDATED, cr, null);
117+
verify(eventHandler, times(0)).handleEvent(any());
118+
119+
cr.getMetadata().setGeneration(6L);
120+
121+
eventSource.eventReceived(ResourceAction.UPDATED, cr, null);
122+
verify(eventHandler, times(1)).handleEvent(any());
123+
}
124+
97125
@Test
98126
public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() {
99127
var config = new TestControllerConfig(
@@ -124,11 +152,25 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() {
124152
verify(eventHandler, times(2)).handleEvent(any());
125153
}
126154

127-
private static class TestControllerConfig extends
128-
DefaultControllerConfiguration<TestCustomResource> {
129-
155+
private static class TestControllerConfig extends ControllerConfig<TestCustomResource> {
130156
public TestControllerConfig(String finalizer, boolean generationAware,
131157
CustomResourceEventFilter<TestCustomResource> eventFilter) {
158+
super(finalizer, generationAware, eventFilter, TestCustomResource.class);
159+
}
160+
}
161+
private static class ObservedGenControllerConfig
162+
extends ControllerConfig<ObservedGenCustomResource> {
163+
public ObservedGenControllerConfig(String finalizer, boolean generationAware,
164+
CustomResourceEventFilter<ObservedGenCustomResource> eventFilter) {
165+
super(finalizer, generationAware, eventFilter, ObservedGenCustomResource.class);
166+
}
167+
}
168+
169+
private static class ControllerConfig<T extends CustomResource<?, ?>> extends
170+
DefaultControllerConfiguration<T> {
171+
172+
public ControllerConfig(String finalizer, boolean generationAware,
173+
CustomResourceEventFilter<T> eventFilter, Class<T> customResourceClass) {
132174
super(
133175
null,
134176
null,
@@ -139,7 +181,7 @@ public TestControllerConfig(String finalizer, boolean generationAware,
139181
null,
140182
null,
141183
eventFilter,
142-
TestCustomResource.class,
184+
customResourceClass,
143185
mock(ConfigurationService.class));
144186

145187
when(getConfigurationService().getResourceCloner())
@@ -158,4 +200,18 @@ public MixedOperation<TestCustomResource, KubernetesResourceList<TestCustomResou
158200
return mock(MixedOperation.class);
159201
}
160202
}
203+
204+
private static class ObservedGenConfiguredController
205+
extends ConfiguredController<ObservedGenCustomResource> {
206+
207+
public ObservedGenConfiguredController(
208+
ControllerConfiguration<ObservedGenCustomResource> configuration) {
209+
super(null, configuration, null);
210+
}
211+
212+
@Override
213+
public MixedOperation<ObservedGenCustomResource, KubernetesResourceList<ObservedGenCustomResource>, Resource<ObservedGenCustomResource>> getCRClient() {
214+
return mock(MixedOperation.class);
215+
}
216+
}
161217
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.javaoperatorsdk.operator.sample.observedgeneration;
2+
3+
import io.fabric8.kubernetes.client.CustomResource;
4+
import io.fabric8.kubernetes.model.annotation.Group;
5+
import io.fabric8.kubernetes.model.annotation.Version;
6+
7+
@Group("sample.javaoperatorsdk.io")
8+
@Version("v1")
9+
public class ObservedGenCustomResource
10+
extends CustomResource<ObservedGenSpec, ObservedGenStatus> {
11+
12+
@Override
13+
protected ObservedGenSpec initSpec() {
14+
return new ObservedGenSpec();
15+
}
16+
17+
@Override
18+
protected ObservedGenStatus initStatus() {
19+
return new ObservedGenStatus();
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.javaoperatorsdk.operator.sample.observedgeneration;
2+
3+
public class ObservedGenSpec {
4+
5+
private String value;
6+
7+
public String getValue() {
8+
return value;
9+
}
10+
11+
public void setValue(String value) {
12+
this.value = value;
13+
}
14+
15+
@Override
16+
public String toString() {
17+
return "TestCustomResourceSpec{" +
18+
"value='" + value + '\'' +
19+
'}';
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.javaoperatorsdk.operator.sample.observedgeneration;
2+
3+
import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus;
4+
5+
public class ObservedGenStatus extends ObservedGenerationAwareStatus {
6+
7+
}

0 commit comments

Comments
 (0)