Skip to content

Commit 8659a44

Browse files
test: cover the configured event recorder end to end
1 parent ab61d3a commit 8659a44

3 files changed

Lines changed: 247 additions & 0 deletions

File tree

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.eventrecorderconfigured;
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.Kind;
22+
import io.fabric8.kubernetes.model.annotation.ShortNames;
23+
import io.fabric8.kubernetes.model.annotation.Version;
24+
25+
@Group("sample.javaoperatorsdk")
26+
@Version("v1")
27+
@Kind("ConfiguredEventRecorderCustomResource")
28+
@ShortNames("cerc")
29+
public class ConfiguredEventRecorderCustomResource extends CustomResource<Void, Void>
30+
implements Namespaced {}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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.eventrecorderconfigured;
17+
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
22+
import org.junit.jupiter.api.AfterEach;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.RegisterExtension;
25+
26+
import io.fabric8.kubernetes.api.model.Event;
27+
import io.fabric8.kubernetes.api.model.HasMetadata;
28+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
29+
import io.fabric8.kubernetes.api.model.OwnerReference;
30+
import io.fabric8.kubernetes.client.KubernetesClient;
31+
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
32+
import io.javaoperatorsdk.annotation.Sample;
33+
import io.javaoperatorsdk.operator.api.event.DefaultEventRecorder;
34+
import io.javaoperatorsdk.operator.api.event.DefaultEventSink;
35+
import io.javaoperatorsdk.operator.api.event.EventKeyStrategy;
36+
import io.javaoperatorsdk.operator.api.event.EventRecord;
37+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
38+
39+
import static io.javaoperatorsdk.operator.baseapi.eventrecorderconfigured.ConfiguredEventRecorderReconciler.AGGREGATED_REASON;
40+
import static io.javaoperatorsdk.operator.baseapi.eventrecorderconfigured.ConfiguredEventRecorderReconciler.NAMED_REASON;
41+
import static org.assertj.core.api.Assertions.assertThat;
42+
import static org.awaitility.Awaitility.await;
43+
44+
@Sample(
45+
tldr = "Configuring how the event recorder aggregates, names and owns events",
46+
description =
47+
"""
48+
Demonstrates configuring the event recorder with a default aggregation key strategy, a \
49+
naming strategy and owner references. Aggregating by reason resolves repeated occurrences \
50+
with changing messages to one event whose count grows and whose message is replaced with \
51+
the latest one. A naming strategy gives events predictable names instead of the default \
52+
identity hash. Owner references relate the events to the object they are about.
53+
""")
54+
class ConfiguredEventRecorderIT {
55+
56+
private static final String TEST_RESOURCE_NAME = "test1";
57+
58+
private final KubernetesClient sinkClient = new KubernetesClientBuilder().build();
59+
60+
private final ConfiguredEventRecorderReconciler reconciler =
61+
new ConfiguredEventRecorderReconciler();
62+
63+
@RegisterExtension
64+
LocallyRunOperatorExtension extension =
65+
LocallyRunOperatorExtension.builder()
66+
.withReconciler(reconciler)
67+
.withConfigurationService(o -> o.withEventRecorder(configuredEventRecorder()))
68+
.build();
69+
70+
@AfterEach
71+
void closeSinkClient() {
72+
sinkClient.close();
73+
}
74+
75+
@Test
76+
void aggregatesOccurrencesWithChangingMessagesOntoOneEvent() {
77+
var resource = extension.create(testResource());
78+
await().untilAsserted(() -> assertThat(reconciler.getNumberOfExecutions()).isPositive());
79+
80+
// reconcile once more: aggregating by reason resolves the new messages to the same events
81+
resource.getMetadata().setAnnotations(Map.of("reconcile", "again"));
82+
extension.replace(resource);
83+
84+
await()
85+
.untilAsserted(
86+
() -> {
87+
assertThat(reconciler.getNumberOfExecutions()).isGreaterThanOrEqualTo(2);
88+
89+
assertThat(eventsWithReason(AGGREGATED_REASON))
90+
.singleElement()
91+
.satisfies(
92+
event -> {
93+
assertThat(event.getCount()).isGreaterThanOrEqualTo(2);
94+
assertThat(event.getMessage())
95+
.startsWith("something changed in execution ");
96+
});
97+
});
98+
}
99+
100+
@Test
101+
void namesEventsThroughTheConfiguredNamingStrategy() {
102+
extension.create(testResource());
103+
104+
await()
105+
.untilAsserted(
106+
() ->
107+
assertThat(eventsWithReason(NAMED_REASON))
108+
.singleElement()
109+
.extracting(event -> event.getMetadata().getName())
110+
.isEqualTo(TEST_RESOURCE_NAME + "-status-report"));
111+
}
112+
113+
@Test
114+
void setsOwnerReferencesOnTheEventsItRecords() {
115+
var resource = extension.create(testResource());
116+
117+
await()
118+
.untilAsserted(
119+
() -> {
120+
var events = eventsForTestResource();
121+
assertThat(events).isNotEmpty();
122+
assertThat(events)
123+
.allSatisfy(
124+
event ->
125+
assertThat(event.getMetadata().getOwnerReferences())
126+
.singleElement()
127+
.returns(
128+
"ConfiguredEventRecorderCustomResource", OwnerReference::getKind)
129+
.returns(resource.getMetadata().getUid(), OwnerReference::getUid));
130+
});
131+
}
132+
133+
private List<Event> eventsWithReason(String reason) {
134+
return eventsForTestResource().stream().filter(e -> reason.equals(e.getReason())).toList();
135+
}
136+
137+
@SuppressWarnings("resource")
138+
private List<Event> eventsForTestResource() {
139+
return extension
140+
.getKubernetesClient()
141+
.v1()
142+
.events()
143+
.inNamespace(extension.getNamespace())
144+
.withField("involvedObject.name", TEST_RESOURCE_NAME)
145+
.list()
146+
.getItems();
147+
}
148+
149+
private ConfiguredEventRecorderCustomResource testResource() {
150+
var resource = new ConfiguredEventRecorderCustomResource();
151+
resource.setMetadata(new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).build());
152+
return resource;
153+
}
154+
155+
private DefaultEventRecorder configuredEventRecorder() {
156+
return DefaultEventRecorder.builder(new DefaultEventSink(sinkClient))
157+
.namingStrategy(ConfiguredEventRecorderIT::statusReportName)
158+
.keyStrategy(EventKeyStrategy.byReason())
159+
.ownerReference(true)
160+
.build();
161+
}
162+
163+
private static Optional<String> statusReportName(HasMetadata regarding, EventRecord record) {
164+
return NAMED_REASON.equals(record.reason())
165+
? Optional.of(regarding.getMetadata().getName() + "-status-report")
166+
: Optional.empty();
167+
}
168+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.eventrecorderconfigured;
17+
18+
import java.util.concurrent.atomic.AtomicInteger;
19+
20+
import io.javaoperatorsdk.operator.api.reconciler.Context;
21+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
22+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
23+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
24+
25+
@ControllerConfiguration(generationAwareEventProcessing = false)
26+
public class ConfiguredEventRecorderReconciler
27+
implements Reconciler<ConfiguredEventRecorderCustomResource> {
28+
29+
public static final String NAMED_REASON = "StatusReport";
30+
public static final String AGGREGATED_REASON = "SomethingChanged";
31+
32+
private final AtomicInteger numberOfExecutions = new AtomicInteger();
33+
34+
@Override
35+
public UpdateControl<ConfiguredEventRecorderCustomResource> reconcile(
36+
ConfiguredEventRecorderCustomResource resource,
37+
Context<ConfiguredEventRecorderCustomResource> context) {
38+
var execution = numberOfExecutions.incrementAndGet();
39+
context.eventRecorder().warn(NAMED_REASON, "status after execution " + execution);
40+
context
41+
.eventRecorder()
42+
.normal(AGGREGATED_REASON, "something changed in execution " + execution);
43+
return UpdateControl.noUpdate();
44+
}
45+
46+
public int getNumberOfExecutions() {
47+
return numberOfExecutions.get();
48+
}
49+
}

0 commit comments

Comments
 (0)