Skip to content

Commit c97d9f6

Browse files
committed
feat: general crd checking activation condition
Signed-off-by: Attila Mészáros <csviri@gmail.com>
1 parent d538711 commit c97d9f6

File tree

2 files changed

+225
-0
lines changed

2 files changed

+225
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package io.javaoperatorsdk.operator.processing.dependent.workflow;
2+
3+
import java.time.Duration;
4+
import java.time.LocalDateTime;
5+
import java.util.Map;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
8+
import io.fabric8.kubernetes.api.model.HasMetadata;
9+
import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
10+
import io.fabric8.kubernetes.client.KubernetesClient;
11+
import io.javaoperatorsdk.operator.api.reconciler.Context;
12+
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
13+
14+
public class CRDPresentActivationCondition<R extends HasMetadata, P extends HasMetadata>
15+
implements Condition<R, P> {
16+
17+
public static final int DEFAULT_CRD_CHECK_LIMIT = 10;
18+
public static final Duration DEFAULT_CRD_CHECK_INTERVAL = Duration.ofSeconds(10);
19+
20+
private static final Map<String, CRDCheckState> crdPresenceCache = new ConcurrentHashMap<>();
21+
22+
private final CRDPresentChecker crdPresentChecker;
23+
private final int checkLimit;
24+
private final Duration crdCheckInterval;
25+
26+
public CRDPresentActivationCondition() {
27+
this(DEFAULT_CRD_CHECK_LIMIT, DEFAULT_CRD_CHECK_INTERVAL);
28+
}
29+
30+
public CRDPresentActivationCondition(int checkLimit, Duration crdCheckInterval) {
31+
this(new CRDPresentChecker(), checkLimit, crdCheckInterval);
32+
}
33+
34+
// for testing purposes only
35+
CRDPresentActivationCondition(CRDPresentChecker crdPresentChecker, int checkLimit,
36+
Duration crdCheckInterval) {
37+
this.crdPresentChecker = crdPresentChecker;
38+
this.checkLimit = checkLimit;
39+
this.crdCheckInterval = crdCheckInterval;
40+
}
41+
42+
@Override
43+
public boolean isMet(DependentResource<R, P> dependentResource,
44+
P primary, Context<P> context) {
45+
46+
var resourceClass = dependentResource.resourceType();
47+
final var crdName = HasMetadata.getFullResourceName(resourceClass);
48+
49+
var crdCheckState = crdPresenceCache.computeIfAbsent(crdName,
50+
g -> new CRDCheckState());
51+
52+
synchronized (crdCheckState) {
53+
if (shouldCheckStateNow(crdCheckState)) {
54+
boolean isPresent = crdPresentChecker
55+
.checkIfCRDPresent(crdName, context.getClient());
56+
crdCheckState.checkedNow(isPresent);
57+
}
58+
}
59+
60+
if (crdCheckState.isCrdPresent() == null) {
61+
throw new IllegalStateException("State should be already checked at this point.");
62+
}
63+
return crdCheckState.isCrdPresent();
64+
}
65+
66+
/**
67+
* Override this method to fine tune when the crd state should be refreshed;
68+
*/
69+
protected boolean shouldCheckStateNow(CRDCheckState crdCheckState) {
70+
if (crdCheckState.isCrdPresent() == null) {
71+
return true;
72+
}
73+
// assumption is that if CRD is present, it is not deleted anymore
74+
if (crdCheckState.isCrdPresent()) {
75+
return false;
76+
}
77+
if (crdCheckState.getCheckCount() >= checkLimit) {
78+
return false;
79+
}
80+
if (crdCheckState.getLastChecked() == null) {
81+
return true;
82+
}
83+
return LocalDateTime.now().isAfter(crdCheckState.getLastChecked().plus(crdCheckInterval));
84+
}
85+
86+
public static class CRDCheckState {
87+
private Boolean crdPresent;
88+
private LocalDateTime lastChecked;
89+
private int checkCount = 0;
90+
91+
public void checkedNow(boolean crdPresent) {
92+
this.crdPresent = crdPresent;
93+
lastChecked = LocalDateTime.now();
94+
checkCount++;
95+
}
96+
97+
public Boolean isCrdPresent() {
98+
return crdPresent;
99+
}
100+
101+
public void setCrdPresent(boolean crdPresent) {
102+
this.crdPresent = crdPresent;
103+
}
104+
105+
public LocalDateTime getLastChecked() {
106+
return lastChecked;
107+
}
108+
109+
public void setLastChecked(LocalDateTime lastChecked) {
110+
this.lastChecked = lastChecked;
111+
}
112+
113+
public int getCheckCount() {
114+
return checkCount;
115+
}
116+
117+
public void setCheckCount(int checkCount) {
118+
this.checkCount = checkCount;
119+
}
120+
}
121+
122+
public static class CRDPresentChecker {
123+
boolean checkIfCRDPresent(String crdName, KubernetesClient client) {
124+
return client.resources(CustomResourceDefinition.class)
125+
.withName(crdName).get() != null;
126+
}
127+
}
128+
129+
/** For testing purposes only */
130+
public static void clearState() {
131+
crdPresenceCache.clear();
132+
}
133+
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package io.javaoperatorsdk.operator.processing.dependent.workflow;
2+
3+
import java.time.Duration;
4+
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
8+
import io.javaoperatorsdk.operator.api.reconciler.Context;
9+
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
10+
import io.javaoperatorsdk.operator.sample.simple.TestCustomResource;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.mockito.ArgumentMatchers.any;
14+
import static org.mockito.Mockito.*;
15+
16+
@SuppressWarnings({"unchecked", "rawtypes"})
17+
class CRDPresentActivationConditionTest {
18+
19+
public static final int TEST_CHECK_INTERVAL = 50;
20+
public static final int TEST_CHECK_INTERVAL_WITH_SLACK = TEST_CHECK_INTERVAL + 10;
21+
private final CRDPresentActivationCondition.CRDPresentChecker checkerMock =
22+
mock(CRDPresentActivationCondition.CRDPresentChecker.class);
23+
private final CRDPresentActivationCondition condition =
24+
new CRDPresentActivationCondition(checkerMock, 2,
25+
Duration.ofMillis(TEST_CHECK_INTERVAL));
26+
private final DependentResource<TestCustomResource, TestCustomResource> dr =
27+
mock(DependentResource.class);
28+
private final Context context = mock(Context.class);
29+
30+
31+
@BeforeEach
32+
void setup() {
33+
CRDPresentActivationCondition.clearState();
34+
when(checkerMock.checkIfCRDPresent(any(), any())).thenReturn(false);
35+
when(dr.resourceType()).thenReturn(TestCustomResource.class);
36+
}
37+
38+
39+
@Test
40+
void checkCRDIfNotCheckedBefore() {
41+
when(checkerMock.checkIfCRDPresent(any(),any())).thenReturn(true);
42+
43+
assertThat(condition.isMet(dr,null,context)).isTrue();
44+
verify(checkerMock, times(1)).checkIfCRDPresent(any(),any());
45+
}
46+
47+
@Test
48+
void instantMetCallSkipsApiCall() {
49+
condition.isMet(dr, null, context);
50+
verify(checkerMock, times(1)).checkIfCRDPresent(any(), any());
51+
52+
condition.isMet(dr, null, context);
53+
verify(checkerMock, times(1)).checkIfCRDPresent(any(), any());
54+
}
55+
56+
@Test
57+
void intervalExpiredAPICheckedAgain() throws InterruptedException {
58+
condition.isMet(dr, null, context);
59+
verify(checkerMock, times(1)).checkIfCRDPresent(any(), any());
60+
61+
Thread.sleep(TEST_CHECK_INTERVAL_WITH_SLACK);
62+
63+
condition.isMet(dr, null, context);
64+
verify(checkerMock, times(2)).checkIfCRDPresent(any(), any());
65+
}
66+
67+
@Test
68+
void crdIsNotCheckedAnymoreIfIfOnceFound() throws InterruptedException {
69+
when(checkerMock.checkIfCRDPresent(any(),any())).thenReturn(true);
70+
71+
condition.isMet(dr,null,context);
72+
verify(checkerMock, times(1)).checkIfCRDPresent(any(),any());
73+
74+
Thread.sleep(TEST_CHECK_INTERVAL_WITH_SLACK);
75+
76+
condition.isMet(dr,null,context);
77+
verify(checkerMock, times(1)).checkIfCRDPresent(any(),any());
78+
}
79+
80+
@Test
81+
void crdNotCheckedAnymoreIfCountExpires() throws InterruptedException {
82+
condition.isMet(dr, null, context);
83+
Thread.sleep(TEST_CHECK_INTERVAL_WITH_SLACK);
84+
condition.isMet(dr, null, context);
85+
Thread.sleep(TEST_CHECK_INTERVAL_WITH_SLACK);
86+
condition.isMet(dr, null, context);
87+
88+
verify(checkerMock, times(2)).checkIfCRDPresent(any(), any());
89+
}
90+
91+
}

0 commit comments

Comments
 (0)