Skip to content

Commit ef78ead

Browse files
committed
feat: create a junit5 extension
1 parent 44b42a1 commit ef78ead

File tree

12 files changed

+375
-19
lines changed

12 files changed

+375
-19
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.io.Closeable;
1212
import java.io.IOException;
1313
import java.util.ArrayList;
14+
import java.util.Collections;
1415
import java.util.List;
1516
import java.util.Objects;
1617
import org.slf4j.Logger;
@@ -48,6 +49,10 @@ public ConfigurationService getConfigurationService() {
4849
return configurationService;
4950
}
5051

52+
public List<ControllerRef> getControllers() {
53+
return Collections.unmodifiableList(controllers);
54+
}
55+
5156
/**
5257
* Finishes the operator startup process. This is mostly used in injection-aware applications
5358
* where there is no obvious entrypoint to the application which can trigger the injection process
@@ -253,13 +258,21 @@ private static <R extends CustomResource> boolean failOnMissingCurrentNS(
253258
return false;
254259
}
255260

256-
private static class ControllerRef {
261+
public static class ControllerRef {
257262
public final ResourceController controller;
258263
public final ControllerConfiguration configuration;
259264

260265
public ControllerRef(ResourceController controller, ControllerConfiguration configuration) {
261266
this.controller = controller;
262267
this.configuration = configuration;
263268
}
269+
270+
public ResourceController getController() {
271+
return controller;
272+
}
273+
274+
public ControllerConfiguration getConfiguration() {
275+
return configuration;
276+
}
264277
}
265278
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
import org.slf4j.Logger;
1111
import org.slf4j.LoggerFactory;
1212

13-
public abstract class AbstractConfigurationService implements ConfigurationService {
13+
public class DefaultConfigurationService implements ConfigurationService {
1414

1515
public static final String LOGGER_NAME = "Default ConfigurationService implementation";
1616
protected static final Logger log = LoggerFactory.getLogger(LOGGER_NAME);
1717

1818
private final Map<String, ControllerConfiguration> configurations = new ConcurrentHashMap<>();
1919
private final Version version;
2020

21-
public AbstractConfigurationService(Version version) {
21+
public DefaultConfigurationService(Version version) {
2222
this.version = version;
2323
}
2424

@@ -54,6 +54,7 @@ protected void throwExceptionOnNameCollision(
5454
+ newControllerClassName);
5555
}
5656

57+
@SuppressWarnings({"unchecked", "rawtypes"})
5758
@Override
5859
public <R extends CustomResource> ControllerConfiguration<R> getConfigurationFor(
5960
ResourceController<R> controller) {

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,15 @@ 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")
45+
.parse(time);
46+
} else {
47+
builtTime = Date.from(Instant.EPOCH);
48+
}
4449
} catch (ParseException e) {
4550
builtTime = Date.from(Instant.EPOCH);
4651
}

operator-framework-junit5/pom.xml

Lines changed: 46 additions & 0 deletions
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.5-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>
Lines changed: 7 additions & 0 deletions
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+
}
Lines changed: 7 additions & 0 deletions
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+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package io.javaoperatorsdk.operator.junit;
2+
3+
import static io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider.override;
4+
5+
import io.fabric8.kubernetes.api.model.KubernetesResourceList;
6+
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
7+
import io.fabric8.kubernetes.client.CustomResource;
8+
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
9+
import io.fabric8.kubernetes.client.KubernetesClient;
10+
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
11+
import io.fabric8.kubernetes.client.dsl.Resource;
12+
import io.javaoperatorsdk.operator.Operator;
13+
import io.javaoperatorsdk.operator.api.ResourceController;
14+
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
15+
import io.javaoperatorsdk.operator.api.config.DefaultConfigurationService;
16+
import io.javaoperatorsdk.operator.processing.retry.Retry;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.UUID;
22+
import java.util.concurrent.TimeUnit;
23+
import java.util.stream.Collectors;
24+
import org.awaitility.Awaitility;
25+
import org.junit.jupiter.api.extension.AfterAllCallback;
26+
import org.junit.jupiter.api.extension.AfterEachCallback;
27+
import org.junit.jupiter.api.extension.BeforeAllCallback;
28+
import org.junit.jupiter.api.extension.BeforeEachCallback;
29+
import org.junit.jupiter.api.extension.ExtensionContext;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
32+
33+
public class OperatorExtension implements
34+
HasKubernetesClient,
35+
BeforeAllCallback, BeforeEachCallback,
36+
AfterAllCallback, AfterEachCallback {
37+
38+
private static final Logger LOGGER = LoggerFactory.getLogger(OperatorExtension.class);
39+
40+
private final KubernetesClient kubernetesClient;
41+
private final ConfigurationService configurationService;
42+
private final String namespace;
43+
private final Operator operator;
44+
private final boolean preserveNamespaceOnError;
45+
46+
private OperatorExtension(ConfigurationService configurationService, boolean preserveNamespaceOnError) {
47+
this.kubernetesClient = new DefaultKubernetesClient();
48+
this.namespace = UUID.randomUUID().toString();
49+
this.configurationService = configurationService;
50+
this.operator = new Operator(this.kubernetesClient, this.configurationService);
51+
this.preserveNamespaceOnError = preserveNamespaceOnError;
52+
}
53+
54+
@Override
55+
public void beforeAll(ExtensionContext context) throws Exception {
56+
LOGGER.info("beforeAll");
57+
before(context);
58+
}
59+
60+
@Override
61+
public void beforeEach(ExtensionContext context) throws Exception {
62+
LOGGER.info("beforeEach");
63+
before(context);
64+
}
65+
66+
@Override
67+
public void afterAll(ExtensionContext context) throws Exception {
68+
LOGGER.info("afterAll");
69+
after(context);
70+
}
71+
72+
@Override
73+
public void afterEach(ExtensionContext context) throws Exception {
74+
LOGGER.info("afterEach");
75+
after(context);
76+
}
77+
78+
@Override
79+
public KubernetesClient getKubernetesClient() {
80+
return kubernetesClient;
81+
}
82+
83+
public String getNamespace() {
84+
return namespace;
85+
}
86+
87+
@SuppressWarnings({"rawtypes"})
88+
public Iterable<ResourceController> controllers() {
89+
return operator.getControllers().stream()
90+
.map(Operator.ControllerRef::getController)
91+
.collect(Collectors.toUnmodifiableList());
92+
}
93+
94+
@SuppressWarnings({"rawtypes"})
95+
public <T extends CustomResource> NonNamespaceOperation<T, KubernetesResourceList<T>, Resource<T>> getResourceClient(
96+
Class<T> type) {
97+
return kubernetesClient.resources(type).inNamespace(namespace);
98+
}
99+
100+
@SuppressWarnings({"rawtypes"})
101+
public <T extends CustomResource> T getCustomResource(Class<T> type, String name) {
102+
return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get();
103+
}
104+
105+
public void register(ResourceController<? extends CustomResource<?, ?>> controller) {
106+
register(controller, null);
107+
}
108+
109+
@SuppressWarnings({"unchecked", "rawtypes"})
110+
public void register(ResourceController controller, Retry retry) {
111+
final var config = configurationService.getConfigurationFor(controller);
112+
final var oconfig = override(config).settingNamespace(namespace);
113+
final var path = "/META-INF/fabric8/" + config.getCRDName() + "-v1.yml";
114+
115+
if (retry != null) {
116+
oconfig.withRetry(retry);
117+
}
118+
119+
try (InputStream is = getClass().getResourceAsStream(path)) {
120+
kubernetesClient.load(is).createOrReplace();
121+
} catch (IOException ex) {
122+
throw new IllegalStateException("Cannot find yaml on classpath: " + path);
123+
}
124+
125+
if (controller instanceof KubernetesClientAware) {
126+
((KubernetesClientAware) controller).setKubernetesClient(kubernetesClient);
127+
}
128+
129+
this.operator.register(controller, oconfig.build());
130+
131+
LOGGER.info("Controller {} is registered", controller.getClass().getCanonicalName());
132+
}
133+
134+
protected void before(ExtensionContext context) {
135+
LOGGER.info("Initializing integration test in namespace {}", namespace);
136+
137+
kubernetesClient.namespaces().create(
138+
new NamespaceBuilder()
139+
.withNewMetadata()
140+
.withName(namespace)
141+
.endMetadata()
142+
.build());
143+
144+
this.operator.start();
145+
}
146+
147+
protected void after(ExtensionContext context) {
148+
if (preserveNamespaceOnError && context.getExecutionException().isPresent()) {
149+
LOGGER.info("Preserving namespace {}", namespace);
150+
} else {
151+
LOGGER.info("Deleting namespace {} and stopping operator", namespace);
152+
kubernetesClient.namespaces().withName(namespace).delete();
153+
Awaitility.await("namespace deleted")
154+
.atMost(45, TimeUnit.SECONDS)
155+
.until(() -> kubernetesClient.namespaces().withName(namespace).get() == null);
156+
}
157+
158+
this.operator.close();
159+
this.kubernetesClient.close();
160+
}
161+
162+
public static Builder builder() {
163+
return new Builder();
164+
}
165+
166+
@SuppressWarnings("rawtypes")
167+
public static class Builder {
168+
private final List<ResourceController> controllers;
169+
private ConfigurationService configurationService;
170+
private boolean preserveNamespaceOnError = false;
171+
172+
protected Builder() {
173+
this.configurationService = new DefaultConfigurationService(null);
174+
this.controllers = new ArrayList<>();
175+
}
176+
177+
public Builder preserveNamespaceOnError(boolean value) {
178+
this.preserveNamespaceOnError = value;
179+
return this;
180+
}
181+
182+
public Builder withConfigurationService(ConfigurationService value) {
183+
configurationService = value;
184+
return this;
185+
}
186+
187+
@SuppressWarnings("rawtypes")
188+
public Builder withController(ResourceController value) {
189+
controllers.add(value);
190+
return this;
191+
}
192+
193+
@SuppressWarnings("rawtypes")
194+
public Builder withController(Class<? extends ResourceController> value) {
195+
try {
196+
controllers.add(value.getConstructor().newInstance());
197+
} catch (Exception e) {
198+
throw new RuntimeException(e);
199+
}
200+
return this;
201+
}
202+
203+
@SuppressWarnings({"rawtypes", "unchecked"})
204+
public OperatorExtension build() {
205+
OperatorExtension answer = new OperatorExtension(configurationService, preserveNamespaceOnError);
206+
for (ResourceController controller: controllers) {
207+
answer.register(controller);
208+
}
209+
210+
return answer;
211+
}
212+
}
213+
}

operator-framework/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,20 @@
7979
<version>0.19</version>
8080
<scope>test</scope>
8181
</dependency>
82+
<dependency>
83+
<groupId>io.javaoperatorsdk</groupId>
84+
<artifactId>operator-framework-junit-5</artifactId>
85+
<version>${project.version}</version>
86+
<scope>test</scope>
87+
</dependency>
88+
<!--
8289
<dependency>
8390
<groupId>org.slf4j</groupId>
8491
<artifactId>slf4j-simple</artifactId>
8592
<version>${slf4j.version}</version>
8693
<scope>test</scope>
8794
</dependency>
95+
-->
8896
<dependency>
8997
<groupId>io.fabric8</groupId>
9098
<artifactId>crd-generator-apt</artifactId>

0 commit comments

Comments
 (0)