Skip to content

Commit 5e34649

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

File tree

12 files changed

+379
-19
lines changed

12 files changed

+379
-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: 8 additions & 4 deletions
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 (ParseException e) {
4549
builtTime = Date.from(Instant.EPOCH);
4650
}

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

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)