Skip to content

Commit c89fcf4

Browse files
committed
wip
Signed-off-by: Attila Mészáros <[email protected]>
1 parent c6cefac commit c89fcf4

File tree

6 files changed

+234
-18
lines changed

6 files changed

+234
-18
lines changed

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

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@
2222
import org.slf4j.event.Level;
2323

2424
import io.fabric8.kubernetes.api.model.HasMetadata;
25-
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
2625
import io.fabric8.kubernetes.client.KubernetesClientException;
27-
import io.fabric8.kubernetes.client.dsl.MixedOperation;
28-
import io.fabric8.kubernetes.client.dsl.Resource;
2926
import io.javaoperatorsdk.operator.OperatorException;
3027
import io.javaoperatorsdk.operator.ReconcilerUtilsInternal;
3128
import io.javaoperatorsdk.operator.api.config.Cloner;
@@ -69,7 +66,6 @@ public ReconciliationDispatcher(Controller<P> controller) {
6966
this(
7067
controller,
7168
new CustomResourceFacade<>(
72-
controller.getCRClient(),
7369
controller.getConfiguration(),
7470
controller.getConfiguration().getConfigurationService().getResourceCloner()));
7571
}
@@ -377,27 +373,14 @@ private void validateExecutionScope(ExecutionScope<P> executionScope) {
377373
// created to support unit testing
378374
static class CustomResourceFacade<R extends HasMetadata> {
379375

380-
private final MixedOperation<R, KubernetesResourceList<R>, Resource<R>> resourceOperation;
381376
private final boolean useSSA;
382377
private final Cloner cloner;
383378

384-
public CustomResourceFacade(
385-
MixedOperation<R, KubernetesResourceList<R>, Resource<R>> resourceOperation,
386-
ControllerConfiguration<R> configuration,
387-
Cloner cloner) {
388-
this.resourceOperation = resourceOperation;
379+
public CustomResourceFacade(ControllerConfiguration<R> configuration, Cloner cloner) {
389380
this.useSSA = configuration.getConfigurationService().useSSAToPatchPrimaryResource();
390381
this.cloner = cloner;
391382
}
392383

393-
public R getResource(String namespace, String name) {
394-
if (namespace != null) {
395-
return resourceOperation.inNamespace(namespace).withName(name).get();
396-
} else {
397-
return resourceOperation.withName(name).get();
398-
}
399-
}
400-
401384
public R patchResource(
402385
Context<R> context, R resource, R originalResource, boolean filterEvent) {
403386
if (log.isDebugEnabled()) {

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,14 @@ public void compareResourcePerformanceTest() {
8181
void retriesAddingFinalizerWithoutSSA() {
8282
// todo
8383
}
84+
85+
@Test
86+
void nullResourceIsGracefullyHandledOnFinalizerRemovalRetry() {
87+
// todo
88+
}
89+
90+
@Test
91+
void retriesFinalizerRemovalWithFreshResource() {
92+
// todo
93+
}
8494
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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.filterpatchevent;
17+
18+
import java.time.Duration;
19+
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.RegisterExtension;
22+
23+
import io.fabric8.kubernetes.api.model.ObjectMeta;
24+
import io.javaoperatorsdk.annotation.Sample;
25+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.awaitility.Awaitility.await;
29+
30+
@Sample(
31+
tldr = "Controlling patch event filtering in UpdateControl",
32+
description =
33+
"""
34+
Demonstrates how to use the filterPatchEvent parameter in UpdateControl to control \
35+
whether patch operations trigger subsequent reconciliation events. When filterPatchEvent \
36+
is true (default), patch events are filtered out to prevent reconciliation loops. When \
37+
false, patch events trigger reconciliation, allowing for controlled event propagation.
38+
""")
39+
class FilterPatchEventIT {
40+
41+
public static final int POLL_DELAY = 150;
42+
public static final String NAME = "test1";
43+
public static final String UPDATED = "updated";
44+
45+
FilterPatchEventTestReconciler reconciler = new FilterPatchEventTestReconciler();
46+
47+
@RegisterExtension
48+
LocallyRunOperatorExtension extension =
49+
LocallyRunOperatorExtension.builder().withReconciler(reconciler).build();
50+
51+
@Test
52+
void patchEventFilteredWhenFlagIsTrue() {
53+
reconciler.setFilterPatchEvent(true);
54+
var resource = createTestResource();
55+
extension.create(resource);
56+
57+
// Wait for the reconciliation to complete and the resource to be updated
58+
await()
59+
.atMost(Duration.ofSeconds(5))
60+
.untilAsserted(
61+
() -> {
62+
var updated = extension.get(FilterPatchEventTestCustomResource.class, NAME);
63+
assertThat(updated.getStatus()).isNotNull();
64+
assertThat(updated.getStatus().getValue()).isEqualTo(UPDATED);
65+
});
66+
67+
// Give some time for any potential additional reconciliations to occur
68+
await().pollDelay(Duration.ofMillis(POLL_DELAY)).untilAsserted(() -> assertThat(true).isTrue());
69+
70+
// With filterPatchEvent=true, reconciliation should only run once
71+
// (triggered by the initial create, but not by the patch operation)
72+
int executions = reconciler.getNumberOfExecutions();
73+
assertThat(executions).isEqualTo(1);
74+
}
75+
76+
@Test
77+
void patchEventNotFilteredWhenFlagIsFalse() {
78+
var resource = createTestResource();
79+
extension.create(resource);
80+
81+
// Wait for the reconciliation to complete and the resource to be updated
82+
await()
83+
.atMost(Duration.ofSeconds(5))
84+
.untilAsserted(
85+
() -> {
86+
var updated = extension.get(FilterPatchEventTestCustomResource.class, NAME);
87+
assertThat(updated.getStatus()).isNotNull();
88+
assertThat(updated.getStatus().getValue()).isEqualTo(UPDATED);
89+
});
90+
91+
// Wait for potential additional reconciliations
92+
await()
93+
.pollDelay(Duration.ofMillis(POLL_DELAY))
94+
.atMost(Duration.ofSeconds(5))
95+
.untilAsserted(
96+
() -> {
97+
int executions = reconciler.getNumberOfExecutions();
98+
// With filterPatchEvent=false, reconciliation should run at least twice
99+
// (once for create and at least once for the patch event)
100+
assertThat(executions).isGreaterThanOrEqualTo(2);
101+
});
102+
}
103+
104+
private FilterPatchEventTestCustomResource createTestResource() {
105+
FilterPatchEventTestCustomResource resource = new FilterPatchEventTestCustomResource();
106+
resource.setMetadata(new ObjectMeta());
107+
resource.getMetadata().setName(NAME);
108+
return resource;
109+
}
110+
}
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.filterpatchevent;
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("fpe")
27+
public class FilterPatchEventTestCustomResource
28+
extends CustomResource<Void, FilterPatchEventTestCustomResourceStatus> implements Namespaced {}
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.filterpatchevent;
17+
18+
public class FilterPatchEventTestCustomResourceStatus {
19+
20+
private String value;
21+
22+
public String getValue() {
23+
return value;
24+
}
25+
26+
public FilterPatchEventTestCustomResourceStatus setValue(String value) {
27+
this.value = value;
28+
return this;
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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.filterpatchevent;
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+
import static io.javaoperatorsdk.operator.baseapi.filterpatchevent.FilterPatchEventIT.UPDATED;
27+
28+
@ControllerConfiguration(generationAwareEventProcessing = false)
29+
public class FilterPatchEventTestReconciler
30+
implements Reconciler<FilterPatchEventTestCustomResource> {
31+
32+
private final AtomicInteger numberOfExecutions = new AtomicInteger(0);
33+
private final AtomicBoolean filterPatchEvent = new AtomicBoolean(false);
34+
35+
@Override
36+
public UpdateControl<FilterPatchEventTestCustomResource> reconcile(
37+
FilterPatchEventTestCustomResource resource,
38+
Context<FilterPatchEventTestCustomResource> context) {
39+
numberOfExecutions.incrementAndGet();
40+
41+
// Update the spec value to trigger a patch operation
42+
resource.setStatus(new FilterPatchEventTestCustomResourceStatus());
43+
resource.getStatus().setValue(UPDATED);
44+
45+
return UpdateControl.patchStatus(resource).filterPatchEvent(filterPatchEvent.get());
46+
}
47+
48+
public int getNumberOfExecutions() {
49+
return numberOfExecutions.get();
50+
}
51+
52+
public void setFilterPatchEvent(boolean b) {
53+
filterPatchEvent.set(b);
54+
}
55+
}

0 commit comments

Comments
 (0)