Skip to content

Commit 470296d

Browse files
committed
wip
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent 6717adf commit 470296d

File tree

6 files changed

+515
-12
lines changed

6 files changed

+515
-12
lines changed

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -725,10 +725,10 @@ private static int validateResourceVersion(String v1) {
725725

726726
/**
727727
* Returns a collector that deduplicates Kubernetes objects by keeping only the one with the
728-
* latest metadata.resourceVersion for each unique name and namespace combination.
729-
* The intended use case is for the rather rare setup when there are overlapping
730-
* {@link io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}s
731-
* for a resource type.
728+
* latest metadata.resourceVersion for each unique name and namespace combination. The intended
729+
* use case is for the rather rare setup when there are overlapping {@link
730+
* io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}s for a
731+
* resource type.
732732
*
733733
* @param <T> the type of HasMetadata objects
734734
* @return a collector that produces a collection of deduplicated Kubernetes objects
@@ -739,10 +739,10 @@ private static int validateResourceVersion(String v1) {
739739

740740
/**
741741
* Returns a collector that deduplicates Kubernetes objects by keeping only the one with the
742-
* latest metadata.resourceVersion for each unique name and namespace combination.
743-
* The intended use case is for the rather rare setup when there are overlapping
744-
* {@link io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}s
745-
* for a resource type.
742+
* latest metadata.resourceVersion for each unique name and namespace combination. The intended
743+
* use case is for the rather rare setup when there are overlapping {@link
744+
* io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}s for a
745+
* resource type.
746746
*
747747
* @param <T> the type of HasMetadata objects
748748
* @return a collector that produces a List of deduplicated Kubernetes objects
@@ -754,10 +754,10 @@ private static int validateResourceVersion(String v1) {
754754

755755
/**
756756
* Returns a collector that deduplicates Kubernetes objects by keeping only the one with the
757-
* latest metadata.resourceVersion for each unique name and namespace combination.
758-
* The intended use case is for the rather rare setup when there are overlapping
759-
* {@link io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}s
760-
* for a resource type.
757+
* latest metadata.resourceVersion for each unique name and namespace combination. The intended
758+
* use case is for the rather rare setup when there are overlapping {@link
759+
* io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}s for a
760+
* resource type.
761761
*
762762
* @param <T> the type of HasMetadata objects
763763
* @return a collector that produces a Set of deduplicated Kubernetes objects
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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.latestdistinct;
17+
18+
import java.time.Duration;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.extension.RegisterExtension;
24+
25+
import io.fabric8.kubernetes.api.model.ConfigMap;
26+
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
27+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
28+
import io.javaoperatorsdk.annotation.Sample;
29+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
30+
31+
import static io.javaoperatorsdk.operator.baseapi.latestdistinct.LatestDistinctTestReconciler.LABEL_KEY;
32+
import static io.javaoperatorsdk.operator.baseapi.latestdistinct.LatestDistinctTestReconciler.LABEL_TYPE_1;
33+
import static io.javaoperatorsdk.operator.baseapi.latestdistinct.LatestDistinctTestReconciler.LABEL_TYPE_2;
34+
import static org.assertj.core.api.Assertions.assertThat;
35+
import static org.awaitility.Awaitility.await;
36+
37+
@Sample(
38+
tldr = "Latest Distinct with Multiple InformerEventSources",
39+
description =
40+
"""
41+
Demonstrates using two separate InformerEventSource instances for ConfigMaps with \
42+
overlapping watches, combined with latestDistinctList() to deduplicate resources by \
43+
keeping the latest version. Also tests ReconcileUtils methods for patching resources \
44+
with proper cache updates.
45+
""")
46+
class LatestDistinctIT {
47+
48+
public static final String TEST_RESOURCE_NAME = "test-resource";
49+
public static final String CONFIG_MAP_1 = "config-map-1";
50+
public static final String CONFIG_MAP_2 = "config-map-2";
51+
public static final String CONFIG_MAP_3 = "config-map-3";
52+
53+
@RegisterExtension
54+
LocallyRunOperatorExtension operator =
55+
LocallyRunOperatorExtension.builder()
56+
.withReconciler(LatestDistinctTestReconciler.class)
57+
.build();
58+
59+
@Test
60+
void testLatestDistinctListWithTwoInformerEventSources() {
61+
// Create the custom resource
62+
var resource = createTestCustomResource();
63+
operator.create(resource);
64+
65+
// Create ConfigMaps with type1 label (watched by first event source)
66+
var cm1 = createConfigMap(CONFIG_MAP_1, LABEL_TYPE_1, resource, "value1");
67+
operator.create(cm1);
68+
69+
var cm2 = createConfigMap(CONFIG_MAP_2, LABEL_TYPE_1, resource, "value2");
70+
operator.create(cm2);
71+
72+
// Create ConfigMap with type2 label (watched by second event source)
73+
var cm3 = createConfigMap(CONFIG_MAP_3, LABEL_TYPE_2, resource, "value3");
74+
operator.create(cm3);
75+
76+
// Wait for reconciliation
77+
var reconciler = operator.getReconcilerOfType(LatestDistinctTestReconciler.class);
78+
await()
79+
.atMost(Duration.ofSeconds(5))
80+
.pollDelay(Duration.ofMillis(300))
81+
.untilAsserted(
82+
() -> {
83+
assertThat(reconciler.getNumberOfExecutions()).isGreaterThanOrEqualTo(1);
84+
var updatedResource =
85+
operator.get(LatestDistinctTestResource.class, TEST_RESOURCE_NAME);
86+
assertThat(updatedResource.getStatus()).isNotNull();
87+
// Should see 3 distinct ConfigMaps
88+
assertThat(updatedResource.getStatus().getConfigMapCount()).isEqualTo(3);
89+
assertThat(updatedResource.getStatus().getDataFromConfigMaps())
90+
.isEqualTo("value1,value2,value3");
91+
// Verify ReconcileUtils was used
92+
assertThat(updatedResource.getStatus().isReconcileUtilsCalled()).isTrue();
93+
});
94+
95+
// Verify distinct ConfigMap names
96+
assertThat(reconciler.getDistinctConfigMapNames())
97+
.containsExactlyInAnyOrder(CONFIG_MAP_1, CONFIG_MAP_2, CONFIG_MAP_3);
98+
}
99+
100+
@Test
101+
void testLatestDistinctDeduplication() {
102+
// Create the custom resource
103+
var resource = createTestCustomResource();
104+
operator.create(resource);
105+
106+
// Create a ConfigMap with type1 label
107+
var cm1 = createConfigMap(CONFIG_MAP_1, LABEL_TYPE_1, resource, "initialValue");
108+
operator.create(cm1);
109+
110+
// Wait for initial reconciliation
111+
var reconciler = operator.getReconcilerOfType(LatestDistinctTestReconciler.class);
112+
await()
113+
.atMost(Duration.ofSeconds(5))
114+
.pollDelay(Duration.ofMillis(300))
115+
.untilAsserted(
116+
() -> {
117+
var updatedResource =
118+
operator.get(LatestDistinctTestResource.class, TEST_RESOURCE_NAME);
119+
assertThat(updatedResource.getStatus()).isNotNull();
120+
assertThat(updatedResource.getStatus().getConfigMapCount()).isEqualTo(1);
121+
assertThat(updatedResource.getStatus().getDataFromConfigMaps())
122+
.isEqualTo("initialValue");
123+
});
124+
125+
int executionsBeforeUpdate = reconciler.getNumberOfExecutions();
126+
127+
// Update the ConfigMap
128+
cm1 = operator.get(ConfigMap.class, CONFIG_MAP_1);
129+
cm1.getData().put("key", "updatedValue");
130+
operator.replace(cm1);
131+
132+
// Wait for reconciliation after update
133+
await()
134+
.atMost(Duration.ofSeconds(5))
135+
.pollDelay(Duration.ofMillis(300))
136+
.untilAsserted(
137+
() -> {
138+
assertThat(reconciler.getNumberOfExecutions()).isGreaterThan(executionsBeforeUpdate);
139+
var updatedResource =
140+
operator.get(LatestDistinctTestResource.class, TEST_RESOURCE_NAME);
141+
assertThat(updatedResource.getStatus()).isNotNull();
142+
// Still should see only 1 distinct ConfigMap (same name, updated version)
143+
assertThat(updatedResource.getStatus().getConfigMapCount()).isEqualTo(1);
144+
assertThat(updatedResource.getStatus().getDataFromConfigMaps())
145+
.isEqualTo("updatedValue");
146+
});
147+
}
148+
149+
@Test
150+
void testReconcileUtilsServerSideApply() {
151+
// Create the custom resource with initial spec value
152+
var resource = createTestCustomResource();
153+
resource.getSpec().setValue("initialSpecValue");
154+
operator.create(resource);
155+
156+
// Create a ConfigMap
157+
var cm1 = createConfigMap(CONFIG_MAP_1, LABEL_TYPE_1, resource, "value1");
158+
operator.create(cm1);
159+
160+
// Wait for reconciliation
161+
var reconciler = operator.getReconcilerOfType(LatestDistinctTestReconciler.class);
162+
await()
163+
.atMost(Duration.ofSeconds(5))
164+
.pollDelay(Duration.ofMillis(300))
165+
.untilAsserted(
166+
() -> {
167+
var updatedResource =
168+
operator.get(LatestDistinctTestResource.class, TEST_RESOURCE_NAME);
169+
assertThat(updatedResource.getStatus()).isNotNull();
170+
assertThat(updatedResource.getStatus().isReconcileUtilsCalled()).isTrue();
171+
// Verify that the status was updated using ReconcileUtils.serverSideApplyStatus
172+
assertThat(updatedResource.getStatus().getConfigMapCount()).isEqualTo(1);
173+
});
174+
175+
// Verify no errors occurred
176+
assertThat(reconciler.isErrorOccurred()).isFalse();
177+
}
178+
179+
private LatestDistinctTestResource createTestCustomResource() {
180+
var resource = new LatestDistinctTestResource();
181+
resource.setMetadata(
182+
new ObjectMetaBuilder()
183+
.withName(TEST_RESOURCE_NAME)
184+
.withNamespace(operator.getNamespace())
185+
.build());
186+
resource.setSpec(new LatestDistinctTestResourceSpec());
187+
return resource;
188+
}
189+
190+
private ConfigMap createConfigMap(
191+
String name, String labelValue, LatestDistinctTestResource owner, String dataValue) {
192+
Map<String, String> labels = new HashMap<>();
193+
labels.put(LABEL_KEY, labelValue);
194+
195+
Map<String, String> data = new HashMap<>();
196+
data.put("key", dataValue);
197+
198+
return new ConfigMapBuilder()
199+
.withMetadata(
200+
new ObjectMetaBuilder()
201+
.withName(name)
202+
.withNamespace(operator.getNamespace())
203+
.withLabels(labels)
204+
.build())
205+
.withData(data)
206+
.withNewMetadata()
207+
.withName(name)
208+
.withNamespace(operator.getNamespace())
209+
.withLabels(labels)
210+
.addNewOwnerReference()
211+
.withApiVersion(owner.getApiVersion())
212+
.withKind(owner.getKind())
213+
.withName(owner.getMetadata().getName())
214+
.withUid(owner.getMetadata().getUid())
215+
.endOwnerReference()
216+
.endMetadata()
217+
.build();
218+
}
219+
}

0 commit comments

Comments
 (0)