Skip to content

Commit 6c6dab2

Browse files
committed
improve: add integration test for PeriodicCleanerExpectation
Signed-off-by: Attila Mészáros <[email protected]>
1 parent b76532e commit 6c6dab2

File tree

4 files changed

+369
-0
lines changed

4 files changed

+369
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.expectation;
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("pcecr")
27+
public class PeriodicCleanerExpectationCustomResource
28+
extends CustomResource<Void, PeriodicCleanerExpectationCustomResourceStatus>
29+
implements Namespaced {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.expectation;
17+
18+
public class PeriodicCleanerExpectationCustomResourceStatus {
19+
20+
private String message;
21+
22+
public String getMessage() {
23+
return message;
24+
}
25+
26+
public void setMessage(String message) {
27+
this.message = message;
28+
}
29+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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.expectation;
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.ObjectMetaBuilder;
24+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
25+
26+
import static io.javaoperatorsdk.operator.baseapi.expectation.PeriodicCleanerExpectationReconciler.DEPLOYMENT_READY;
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.awaitility.Awaitility.await;
29+
30+
/**
31+
* Integration test showcasing PeriodicCleanerExpectationManager usage.
32+
*
33+
* <p>This test demonstrates the key benefits of PeriodicCleanerExpectationManager: 1. Works without
34+
* requiring @ControllerConfiguration(triggerReconcilerOnAllEvents = true) 2. Automatically cleans
35+
* up stale expectations periodically 3. Maintains the same expectation API and functionality as the
36+
* regular ExpectationManager
37+
*/
38+
class PeriodicCleanerExpectationIT {
39+
40+
public static final String TEST_1 = "test1";
41+
public static final String TEST_2 = "test2";
42+
43+
@RegisterExtension
44+
LocallyRunOperatorExtension extension =
45+
LocallyRunOperatorExtension.builder()
46+
.withReconciler(new PeriodicCleanerExpectationReconciler())
47+
.build();
48+
49+
@Test
50+
void testPeriodicCleanerExpectationBasicFlow() {
51+
extension
52+
.getReconcilerOfType(PeriodicCleanerExpectationReconciler.class)
53+
.setTimeout(Duration.ofSeconds(30));
54+
var res = testResource();
55+
extension.create(res);
56+
57+
await()
58+
.untilAsserted(
59+
() -> {
60+
var actual = extension.get(PeriodicCleanerExpectationCustomResource.class, TEST_1);
61+
assertThat(actual.getStatus()).isNotNull();
62+
assertThat(actual.getStatus().getMessage()).isEqualTo(DEPLOYMENT_READY);
63+
});
64+
}
65+
66+
@Test
67+
void testPeriodicCleanerExpectationTimeouts() {
68+
extension
69+
.getReconcilerOfType(PeriodicCleanerExpectationReconciler.class)
70+
.setTimeout(Duration.ofMillis(300));
71+
var res = testResource();
72+
extension.create(res);
73+
74+
await()
75+
.untilAsserted(
76+
() -> {
77+
var actual = extension.get(PeriodicCleanerExpectationCustomResource.class, TEST_1);
78+
assertThat(actual.getStatus()).isNotNull();
79+
assertThat(actual.getStatus().getMessage()).isEqualTo(DEPLOYMENT_READY);
80+
});
81+
}
82+
83+
@Test
84+
void demonstratesNoTriggerReconcilerOnAllEventsNeeded() {
85+
// This test demonstrates that PeriodicCleanerExpectationManager works
86+
// without @ControllerConfiguration(triggerReconcilerOnAllEvents = true)
87+
88+
// The PeriodicCleanerExpectationReconciler doesn't use triggerReconcilerOnAllEvents = true
89+
// yet expectations still work properly due to the periodic cleanup functionality
90+
91+
var reconciler = extension.getReconcilerOfType(PeriodicCleanerExpectationReconciler.class);
92+
reconciler.setTimeout(Duration.ofSeconds(30));
93+
94+
var res = testResource("no-trigger-test");
95+
extension.create(res);
96+
97+
// Verify that expectations work even without triggerReconcilerOnAllEvents = true
98+
await()
99+
.untilAsserted(
100+
() -> {
101+
var actual =
102+
extension.get(PeriodicCleanerExpectationCustomResource.class, "no-trigger-test");
103+
assertThat(actual.getStatus()).isNotNull();
104+
assertThat(actual.getStatus().getMessage()).isEqualTo(DEPLOYMENT_READY);
105+
});
106+
}
107+
108+
private PeriodicCleanerExpectationCustomResource testResource() {
109+
return testResource(TEST_1);
110+
}
111+
112+
private PeriodicCleanerExpectationCustomResource testResource(String name) {
113+
var res = new PeriodicCleanerExpectationCustomResource();
114+
res.setMetadata(new ObjectMetaBuilder().withName(name).build());
115+
return res;
116+
}
117+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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.expectation;
17+
18+
import java.time.Duration;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.concurrent.atomic.AtomicReference;
22+
23+
import io.fabric8.kubernetes.api.model.ContainerBuilder;
24+
import io.fabric8.kubernetes.api.model.ContainerPortBuilder;
25+
import io.fabric8.kubernetes.api.model.LabelSelectorBuilder;
26+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
27+
import io.fabric8.kubernetes.api.model.PodSpecBuilder;
28+
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
29+
import io.fabric8.kubernetes.api.model.apps.Deployment;
30+
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
31+
import io.fabric8.kubernetes.api.model.apps.DeploymentSpecBuilder;
32+
import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration;
33+
import io.javaoperatorsdk.operator.api.reconciler.Context;
34+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
35+
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
36+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
37+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
38+
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
39+
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
40+
import io.javaoperatorsdk.operator.processing.expectation.Expectation;
41+
import io.javaoperatorsdk.operator.processing.expectation.PeriodicCleanerExpectationManager;
42+
43+
/**
44+
* Integration test reconciler showcasing PeriodicCleanerExpectationManager usage.
45+
*
46+
* <p>Key differences from ExpectationReconciler: - Uses PeriodicCleanerExpectationManager instead
47+
* of ExpectationManager - Does NOT use @ControllerConfiguration(triggerReconcilerOnAllEvents =
48+
* true) - Demonstrates periodic cleanup functionality without requiring reconciler triggers on all
49+
* events
50+
*/
51+
@ControllerConfiguration
52+
public class PeriodicCleanerExpectationReconciler
53+
implements Reconciler<PeriodicCleanerExpectationCustomResource> {
54+
55+
public static final String DEPLOYMENT_READY = "Deployment ready";
56+
public static final String DEPLOYMENT_TIMEOUT = "Deployment timeout";
57+
public static final String DEPLOYMENT_READY_EXPECTATION_NAME = "deploymentReadyExpectation";
58+
59+
private PeriodicCleanerExpectationManager<PeriodicCleanerExpectationCustomResource>
60+
expectationManager;
61+
private final AtomicReference<Duration> timeoutRef =
62+
new AtomicReference<>(Duration.ofSeconds(30));
63+
64+
public PeriodicCleanerExpectationReconciler() {
65+
// expectationManager will be initialized in prepareEventSources when cache is available
66+
}
67+
68+
public void setTimeout(Duration timeout) {
69+
timeoutRef.set(timeout);
70+
}
71+
72+
public PeriodicCleanerExpectationManager<PeriodicCleanerExpectationCustomResource>
73+
getExpectationManager() {
74+
return expectationManager;
75+
}
76+
77+
@Override
78+
public UpdateControl<PeriodicCleanerExpectationCustomResource> reconcile(
79+
PeriodicCleanerExpectationCustomResource primary,
80+
Context<PeriodicCleanerExpectationCustomResource> context) {
81+
82+
// Note: Unlike regular ExpectationManager, we don't need to manually clean up on delete
83+
// because PeriodicCleanerExpectationManager handles this automatically via periodic cleanup
84+
85+
// exiting asap if there is an expectation that is not timed out neither fulfilled
86+
if (expectationManager.ongoingExpectationPresent(primary, context)) {
87+
return UpdateControl.noUpdate();
88+
}
89+
90+
var deployment = context.getSecondaryResource(Deployment.class);
91+
if (deployment.isEmpty()) {
92+
createDeployment(primary, context);
93+
var set =
94+
expectationManager.checkAndSetExpectation(
95+
primary, context, timeoutRef.get(), deploymentReadyExpectation());
96+
if (set) {
97+
return UpdateControl.noUpdate();
98+
}
99+
} else {
100+
// Checks the expectation and removes it once it is fulfilled.
101+
// In your logic you might add a next expectation based on your workflow.
102+
// Expectations have a name, so you can easily distinguish multiple expectations.
103+
var res =
104+
expectationManager.checkExpectation(DEPLOYMENT_READY_EXPECTATION_NAME, primary, context);
105+
// note that this happens only once, since if the expectation is fulfilled, it is also removed
106+
// from the manager
107+
if (res.isFulfilled()) {
108+
return patchStatusWithMessage(primary, DEPLOYMENT_READY);
109+
} else if (res.isTimedOut()) {
110+
// you might add some other timeout handling here
111+
return patchStatusWithMessage(primary, DEPLOYMENT_TIMEOUT);
112+
}
113+
}
114+
return UpdateControl.noUpdate();
115+
}
116+
117+
@Override
118+
public List<EventSource<?, PeriodicCleanerExpectationCustomResource>> prepareEventSources(
119+
EventSourceContext<PeriodicCleanerExpectationCustomResource> context) {
120+
121+
// Initialize expectationManager with primary cache from the context
122+
// Use a short period (1 second) for faster testing
123+
var primaryCache = context.getPrimaryCache();
124+
this.expectationManager =
125+
new PeriodicCleanerExpectationManager<>(Duration.ofSeconds(1), primaryCache);
126+
127+
return List.of(
128+
new InformerEventSource<>(
129+
InformerEventSourceConfiguration.from(
130+
Deployment.class, PeriodicCleanerExpectationCustomResource.class)
131+
.build(),
132+
context));
133+
}
134+
135+
private static void createDeployment(
136+
PeriodicCleanerExpectationCustomResource primary,
137+
Context<PeriodicCleanerExpectationCustomResource> context) {
138+
var deployment =
139+
new DeploymentBuilder()
140+
.withMetadata(
141+
new ObjectMetaBuilder()
142+
.withName(primary.getMetadata().getName())
143+
.withNamespace(primary.getMetadata().getNamespace())
144+
.build())
145+
.withSpec(
146+
new DeploymentSpecBuilder()
147+
.withReplicas(3)
148+
.withSelector(
149+
new LabelSelectorBuilder().withMatchLabels(Map.of("app", "nginx")).build())
150+
.withTemplate(
151+
new PodTemplateSpecBuilder()
152+
.withMetadata(
153+
new ObjectMetaBuilder().withLabels(Map.of("app", "nginx")).build())
154+
.withSpec(
155+
new PodSpecBuilder()
156+
.withContainers(
157+
new ContainerBuilder()
158+
.withName("nginx")
159+
.withImage("nginx:1.29.2")
160+
.withPorts(
161+
new ContainerPortBuilder()
162+
.withContainerPort(80)
163+
.build())
164+
.build())
165+
.build())
166+
.build())
167+
.build())
168+
.build();
169+
deployment.addOwnerReference(primary);
170+
context.getClient().resource(deployment).serverSideApply();
171+
}
172+
173+
private static Expectation<PeriodicCleanerExpectationCustomResource>
174+
deploymentReadyExpectation() {
175+
return Expectation.createExpectation(
176+
DEPLOYMENT_READY_EXPECTATION_NAME,
177+
(primary, context) ->
178+
context
179+
.getSecondaryResource(Deployment.class)
180+
.map(
181+
ad ->
182+
ad.getStatus() != null
183+
&& ad.getStatus().getReadyReplicas() != null
184+
&& ad.getStatus().getReadyReplicas() == 3)
185+
.orElse(false));
186+
}
187+
188+
private static UpdateControl<PeriodicCleanerExpectationCustomResource> patchStatusWithMessage(
189+
PeriodicCleanerExpectationCustomResource primary, String message) {
190+
primary.setStatus(new PeriodicCleanerExpectationCustomResourceStatus());
191+
primary.getStatus().setMessage(message);
192+
return UpdateControl.patchStatus(primary);
193+
}
194+
}

0 commit comments

Comments
 (0)