Skip to content

Commit 7165d2c

Browse files
committed
feat: introduce Junit5 extension
1 parent 646f396 commit 7165d2c

25 files changed

+680
-555
lines changed

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.io.Closeable;
44
import java.io.IOException;
55
import java.net.ConnectException;
6+
import java.util.Collections;
67
import java.util.LinkedList;
78
import java.util.List;
89
import java.util.concurrent.locks.ReentrantLock;
@@ -57,6 +58,10 @@ public ConfigurationService getConfigurationService() {
5758
return configurationService;
5859
}
5960

61+
public List<ConfiguredController> getControllers() {
62+
return Collections.unmodifiableList(controllers.controllers);
63+
}
64+
6065
/**
6166
* Finishes the operator startup process. This is mostly used in injection-aware applications
6267
* where there is no obvious entrypoint to the application which can trigger the injection process
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.javaoperatorsdk.operator.api.config;
2+
3+
public class BaseConfigurationService extends AbstractConfigurationService {
4+
5+
public BaseConfigurationService(Version version) {
6+
super(version);
7+
}
8+
}

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java

+8-4
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,14 @@ public static Version loadFromProperties() {
3737

3838
Date builtTime;
3939
try {
40-
builtTime =
41-
// RFC 822 date is the default format used by git-commit-id-plugin
42-
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
43-
.parse(properties.getProperty("git.build.time"));
40+
String time = properties.getProperty("git.build.time");
41+
if (time != null) {
42+
builtTime =
43+
// RFC 822 date is the default format used by git-commit-id-plugin
44+
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(time);
45+
} else {
46+
builtTime = Date.from(Instant.EPOCH);
47+
}
4448
} catch (Exception e) {
4549
log.debug("Couldn't parse git.build.time property", e);
4650
builtTime = Date.from(Instant.EPOCH);

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Version.java

+3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package io.javaoperatorsdk.operator.api.config;
22

3+
import java.time.Instant;
34
import java.util.Date;
45

56
/** A class encapsulating the version information associated with this SDK instance. */
67
public class Version {
78

9+
public static final Version UNKNOWN = new Version("unknown", "unknown", Date.from(Instant.EPOCH));
10+
811
private final String sdk;
912
private final String commit;
1013
private final Date builtTime;

Diff for: operator-framework-junit5/pom.xml

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<artifactId>java-operator-sdk</artifactId>
7+
<groupId>io.javaoperatorsdk</groupId>
8+
<version>1.9.7-SNAPSHOT</version>
9+
</parent>
10+
<modelVersion>4.0.0</modelVersion>
11+
12+
<artifactId>operator-framework-junit-5</artifactId>
13+
<name>Operator SDK - Framework - Junit5</name>
14+
15+
<properties>
16+
<maven.compiler.source>11</maven.compiler.source>
17+
<maven.compiler.target>11</maven.compiler.target>
18+
</properties>
19+
20+
<dependencies>
21+
<dependency>
22+
<groupId>io.javaoperatorsdk</groupId>
23+
<artifactId>operator-framework-core</artifactId>
24+
<version>${project.version}</version>
25+
</dependency>
26+
<dependency>
27+
<groupId>org.junit.jupiter</groupId>
28+
<artifactId>junit-jupiter-api</artifactId>
29+
</dependency>
30+
<dependency>
31+
<groupId>org.junit.jupiter</groupId>
32+
<artifactId>junit-jupiter-engine</artifactId>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.assertj</groupId>
36+
<artifactId>assertj-core</artifactId>
37+
<version>3.20.2</version>
38+
</dependency>
39+
<dependency>
40+
<groupId>org.awaitility</groupId>
41+
<artifactId>awaitility</artifactId>
42+
<version>4.1.0</version>
43+
</dependency>
44+
</dependencies>
45+
46+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.javaoperatorsdk.operator.junit;
2+
3+
import io.fabric8.kubernetes.client.KubernetesClient;
4+
5+
public interface HasKubernetesClient {
6+
KubernetesClient getKubernetesClient();
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.javaoperatorsdk.operator.junit;
2+
3+
import io.fabric8.kubernetes.client.KubernetesClient;
4+
5+
public interface KubernetesClientAware extends HasKubernetesClient {
6+
void setKubernetesClient(KubernetesClient kubernetesClient);
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
package io.javaoperatorsdk.operator.junit;
2+
3+
import io.fabric8.kubernetes.api.model.HasMetadata;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.UUID;
9+
import java.util.concurrent.TimeUnit;
10+
import java.util.stream.Collectors;
11+
12+
import org.awaitility.Awaitility;
13+
import org.junit.jupiter.api.extension.AfterAllCallback;
14+
import org.junit.jupiter.api.extension.AfterEachCallback;
15+
import org.junit.jupiter.api.extension.BeforeAllCallback;
16+
import org.junit.jupiter.api.extension.BeforeEachCallback;
17+
import org.junit.jupiter.api.extension.ExtensionContext;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
22+
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
23+
import io.fabric8.kubernetes.client.CustomResource;
24+
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
25+
import io.fabric8.kubernetes.client.KubernetesClient;
26+
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
27+
import io.fabric8.kubernetes.client.dsl.Resource;
28+
import io.fabric8.kubernetes.client.utils.Utils;
29+
import io.javaoperatorsdk.operator.Operator;
30+
import io.javaoperatorsdk.operator.api.ResourceController;
31+
import io.javaoperatorsdk.operator.api.config.BaseConfigurationService;
32+
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
33+
import io.javaoperatorsdk.operator.api.config.Version;
34+
import io.javaoperatorsdk.operator.processing.ConfiguredController;
35+
import io.javaoperatorsdk.operator.processing.retry.Retry;
36+
37+
import static io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider.override;
38+
39+
public class OperatorExtension
40+
implements HasKubernetesClient,
41+
BeforeAllCallback,
42+
BeforeEachCallback,
43+
AfterAllCallback,
44+
AfterEachCallback {
45+
46+
private static final Logger LOGGER = LoggerFactory.getLogger(OperatorExtension.class);
47+
48+
private final KubernetesClient kubernetesClient;
49+
private final ConfigurationService configurationService;
50+
private final String namespace;
51+
private final Operator operator;
52+
private final boolean preserveNamespaceOnError;
53+
private final boolean waitForNamespaceDeletion;
54+
55+
private OperatorExtension(
56+
ConfigurationService configurationService, boolean preserveNamespaceOnError,
57+
boolean waitForNamespaceDeletion) {
58+
this.kubernetesClient = new DefaultKubernetesClient();
59+
this.namespace = UUID.randomUUID().toString();
60+
this.configurationService = configurationService;
61+
this.operator = new Operator(this.kubernetesClient, this.configurationService);
62+
this.preserveNamespaceOnError = preserveNamespaceOnError;
63+
this.waitForNamespaceDeletion = waitForNamespaceDeletion;
64+
}
65+
66+
public static Builder builder() {
67+
return new Builder();
68+
}
69+
70+
@Override
71+
public void beforeAll(ExtensionContext context) throws Exception {
72+
before(context);
73+
}
74+
75+
@Override
76+
public void beforeEach(ExtensionContext context) throws Exception {
77+
before(context);
78+
}
79+
80+
@Override
81+
public void afterAll(ExtensionContext context) throws Exception {
82+
after(context);
83+
}
84+
85+
@Override
86+
public void afterEach(ExtensionContext context) throws Exception {
87+
after(context);
88+
}
89+
90+
@Override
91+
public KubernetesClient getKubernetesClient() {
92+
return kubernetesClient;
93+
}
94+
95+
public String getNamespace() {
96+
return namespace;
97+
}
98+
99+
@SuppressWarnings({"rawtypes"})
100+
public List<ResourceController> getControllers() {
101+
return operator.getControllers().stream()
102+
.map(ConfiguredController::getController)
103+
.collect(Collectors.toUnmodifiableList());
104+
}
105+
106+
@SuppressWarnings({"rawtypes"})
107+
public <T extends ResourceController> T getControllerOfType(Class<T> type) {
108+
return operator.getControllers().stream()
109+
.map(ConfiguredController::getController)
110+
.filter(type::isInstance)
111+
.map(type::cast)
112+
.findFirst()
113+
.orElseThrow(
114+
() -> new IllegalArgumentException("Unable to find a controller of type: " + type));
115+
}
116+
117+
public <T extends HasMetadata> NonNamespaceOperation<T, KubernetesResourceList<T>, Resource<T>> getResourceClient(
118+
Class<T> type) {
119+
return kubernetesClient.resources(type).inNamespace(namespace);
120+
}
121+
122+
@SuppressWarnings({"rawtypes"})
123+
public <T extends CustomResource> T getCustomResource(Class<T> type, String name) {
124+
return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get();
125+
}
126+
127+
public void register(ResourceController<? extends CustomResource<?, ?>> controller) {
128+
register(controller, null);
129+
}
130+
131+
@SuppressWarnings({"unchecked", "rawtypes"})
132+
public void register(ResourceController controller, Retry retry) {
133+
final var config = configurationService.getConfigurationFor(controller);
134+
final var oconfig = override(config).settingNamespace(namespace);
135+
final var path = "/META-INF/fabric8/" + config.getCRDName() + "-v1.yml";
136+
137+
if (retry != null) {
138+
oconfig.withRetry(retry);
139+
}
140+
141+
try (InputStream is = getClass().getResourceAsStream(path)) {
142+
kubernetesClient.load(is).createOrReplace();
143+
} catch (IOException ex) {
144+
throw new IllegalStateException("Cannot find yaml on classpath: " + path);
145+
}
146+
147+
if (controller instanceof KubernetesClientAware) {
148+
((KubernetesClientAware) controller).setKubernetesClient(kubernetesClient);
149+
}
150+
151+
this.operator.register(controller, oconfig.build());
152+
153+
LOGGER.info("Controller {} is registered", controller.getClass().getCanonicalName());
154+
}
155+
156+
protected void before(ExtensionContext context) {
157+
LOGGER.info("Initializing integration test in namespace {}", namespace);
158+
159+
kubernetesClient
160+
.namespaces()
161+
.create(new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build());
162+
163+
this.operator.start();
164+
}
165+
166+
protected void after(ExtensionContext context) {
167+
if (preserveNamespaceOnError && context.getExecutionException().isPresent()) {
168+
LOGGER.info("Preserving namespace {}", namespace);
169+
} else {
170+
LOGGER.info("Deleting namespace {} and stopping operator", namespace);
171+
kubernetesClient.namespaces().withName(namespace).delete();
172+
if (waitForNamespaceDeletion) {
173+
LOGGER.info("Waiting for namespace {} to be deleted", namespace);
174+
Awaitility.await("namespace deleted")
175+
.pollInterval(50, TimeUnit.MILLISECONDS)
176+
.atMost(60, TimeUnit.SECONDS)
177+
.until(() -> kubernetesClient.namespaces().withName(namespace).get() == null);
178+
}
179+
}
180+
181+
try {
182+
this.operator.close();
183+
} catch (Exception e) {
184+
// ignored
185+
}
186+
try {
187+
this.kubernetesClient.close();
188+
} catch (Exception e) {
189+
// ignored
190+
}
191+
}
192+
193+
@SuppressWarnings("rawtypes")
194+
public static class Builder {
195+
private final List<ControllerSpec> controllers;
196+
private ConfigurationService configurationService;
197+
private boolean preserveNamespaceOnError;
198+
private boolean waitForNamespaceDeletion;
199+
200+
protected Builder() {
201+
this.configurationService = new BaseConfigurationService(Version.UNKNOWN);
202+
this.controllers = new ArrayList<>();
203+
204+
this.preserveNamespaceOnError = Utils.getSystemPropertyOrEnvVar(
205+
"josdk.it.preserveNamespaceOnError",
206+
false);
207+
208+
this.waitForNamespaceDeletion = Utils.getSystemPropertyOrEnvVar(
209+
"josdk.it.waitForNamespaceDeletion",
210+
true);
211+
}
212+
213+
public Builder preserveNamespaceOnError(boolean value) {
214+
this.preserveNamespaceOnError = value;
215+
return this;
216+
}
217+
218+
public Builder waitForNamespaceDeletion(boolean value) {
219+
this.waitForNamespaceDeletion = value;
220+
return this;
221+
}
222+
223+
public Builder withConfigurationService(ConfigurationService value) {
224+
configurationService = value;
225+
return this;
226+
}
227+
228+
@SuppressWarnings("rawtypes")
229+
public Builder withController(ResourceController value) {
230+
controllers.add(new ControllerSpec(value, null));
231+
return this;
232+
}
233+
234+
@SuppressWarnings("rawtypes")
235+
public Builder withController(ResourceController value, Retry retry) {
236+
controllers.add(new ControllerSpec(value, retry));
237+
return this;
238+
}
239+
240+
@SuppressWarnings("rawtypes")
241+
public Builder withController(Class<? extends ResourceController> value) {
242+
try {
243+
controllers.add(new ControllerSpec(value.getConstructor().newInstance(), null));
244+
} catch (Exception e) {
245+
throw new RuntimeException(e);
246+
}
247+
return this;
248+
}
249+
250+
@SuppressWarnings({"rawtypes", "unchecked"})
251+
public OperatorExtension build() {
252+
OperatorExtension answer =
253+
new OperatorExtension(configurationService, preserveNamespaceOnError,
254+
waitForNamespaceDeletion);
255+
for (ControllerSpec spec : controllers) {
256+
answer.register(spec.controller, spec.retry);
257+
}
258+
259+
return answer;
260+
}
261+
262+
private static class ControllerSpec {
263+
final ResourceController controller;
264+
final Retry retry;
265+
266+
public ControllerSpec(
267+
ResourceController controller,
268+
Retry retry) {
269+
this.controller = controller;
270+
this.retry = retry;
271+
}
272+
}
273+
}
274+
}

0 commit comments

Comments
 (0)