-
Notifications
You must be signed in to change notification settings - Fork 220
feat: create a junit5 extension #507
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ | |
import java.io.Closeable; | ||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import org.slf4j.Logger; | ||
|
@@ -48,6 +49,10 @@ public ConfigurationService getConfigurationService() { | |
return configurationService; | ||
} | ||
|
||
public List<ControllerRef> getControllers() { | ||
return Collections.unmodifiableList(controllers); | ||
} | ||
|
||
/** | ||
* Finishes the operator startup process. This is mostly used in injection-aware applications | ||
* 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( | |
return false; | ||
} | ||
|
||
private static class ControllerRef { | ||
public static class ControllerRef { | ||
public final ResourceController controller; | ||
public final ControllerConfiguration configuration; | ||
|
||
public ControllerRef(ResourceController controller, ControllerConfiguration configuration) { | ||
this.controller = controller; | ||
this.configuration = configuration; | ||
} | ||
|
||
public ResourceController getController() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why add getters if the instance variables are public final… Use one or the other :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a leftover when using the |
||
return controller; | ||
} | ||
|
||
public ControllerConfiguration getConfiguration() { | ||
return configuration; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<parent> | ||
<artifactId>java-operator-sdk</artifactId> | ||
<groupId>io.javaoperatorsdk</groupId> | ||
<version>1.9.5-SNAPSHOT</version> | ||
</parent> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<artifactId>operator-framework-junit-5</artifactId> | ||
<name>Operator SDK - Framework - Junit5</name> | ||
|
||
<properties> | ||
<maven.compiler.source>11</maven.compiler.source> | ||
<maven.compiler.target>11</maven.compiler.target> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>io.javaoperatorsdk</groupId> | ||
<artifactId>operator-framework-core</artifactId> | ||
<version>${project.version}</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter-api</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter-engine</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.assertj</groupId> | ||
<artifactId>assertj-core</artifactId> | ||
<version>3.20.2</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.awaitility</groupId> | ||
<artifactId>awaitility</artifactId> | ||
<version>4.1.0</version> | ||
</dependency> | ||
</dependencies> | ||
|
||
</project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package io.javaoperatorsdk.operator.junit; | ||
|
||
import io.fabric8.kubernetes.client.KubernetesClient; | ||
|
||
public interface HasKubernetesClient { | ||
KubernetesClient getKubernetesClient(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package io.javaoperatorsdk.operator.junit; | ||
|
||
import io.fabric8.kubernetes.client.KubernetesClient; | ||
|
||
public interface KubernetesClientAware extends HasKubernetesClient { | ||
void setKubernetesClient(KubernetesClient kubernetesClient); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
package io.javaoperatorsdk.operator.junit; | ||
|
||
import static io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider.override; | ||
|
||
import io.fabric8.kubernetes.api.model.KubernetesResourceList; | ||
import io.fabric8.kubernetes.api.model.NamespaceBuilder; | ||
import io.fabric8.kubernetes.client.CustomResource; | ||
import io.fabric8.kubernetes.client.DefaultKubernetesClient; | ||
import io.fabric8.kubernetes.client.KubernetesClient; | ||
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; | ||
import io.fabric8.kubernetes.client.dsl.Resource; | ||
import io.javaoperatorsdk.operator.Operator; | ||
import io.javaoperatorsdk.operator.api.ResourceController; | ||
import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; | ||
import io.javaoperatorsdk.operator.api.config.ConfigurationService; | ||
import io.javaoperatorsdk.operator.api.config.Version; | ||
import io.javaoperatorsdk.operator.processing.retry.Retry; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.UUID; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.stream.Collectors; | ||
import org.awaitility.Awaitility; | ||
import org.junit.jupiter.api.extension.AfterAllCallback; | ||
import org.junit.jupiter.api.extension.AfterEachCallback; | ||
import org.junit.jupiter.api.extension.BeforeAllCallback; | ||
import org.junit.jupiter.api.extension.BeforeEachCallback; | ||
import org.junit.jupiter.api.extension.ExtensionContext; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public class OperatorExtension | ||
implements HasKubernetesClient, | ||
BeforeAllCallback, | ||
BeforeEachCallback, | ||
AfterAllCallback, | ||
AfterEachCallback { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(OperatorExtension.class); | ||
|
||
private final KubernetesClient kubernetesClient; | ||
private final ConfigurationService configurationService; | ||
private final String namespace; | ||
private final Operator operator; | ||
private final boolean preserveNamespaceOnError; | ||
|
||
private OperatorExtension( | ||
ConfigurationService configurationService, boolean preserveNamespaceOnError) { | ||
this.kubernetesClient = new DefaultKubernetesClient(); | ||
this.namespace = UUID.randomUUID().toString(); | ||
this.configurationService = configurationService; | ||
this.operator = new Operator(this.kubernetesClient, this.configurationService); | ||
this.preserveNamespaceOnError = preserveNamespaceOnError; | ||
} | ||
|
||
@Override | ||
public void beforeAll(ExtensionContext context) throws Exception { | ||
before(context); | ||
} | ||
|
||
@Override | ||
public void beforeEach(ExtensionContext context) throws Exception { | ||
before(context); | ||
} | ||
|
||
@Override | ||
public void afterAll(ExtensionContext context) throws Exception { | ||
after(context); | ||
} | ||
|
||
@Override | ||
public void afterEach(ExtensionContext context) throws Exception { | ||
after(context); | ||
} | ||
|
||
@Override | ||
public KubernetesClient getKubernetesClient() { | ||
return kubernetesClient; | ||
} | ||
|
||
public String getNamespace() { | ||
return namespace; | ||
} | ||
|
||
@SuppressWarnings({"rawtypes"}) | ||
public List<ResourceController> controllers() { | ||
return operator.getControllers().stream() | ||
.map(Operator.ControllerRef::getController) | ||
.collect(Collectors.toUnmodifiableList()); | ||
} | ||
|
||
@SuppressWarnings({"rawtypes"}) | ||
public <T extends CustomResource> | ||
NonNamespaceOperation<T, KubernetesResourceList<T>, Resource<T>> getResourceClient( | ||
Class<T> type) { | ||
return kubernetesClient.resources(type).inNamespace(namespace); | ||
} | ||
|
||
@SuppressWarnings({"rawtypes"}) | ||
public <T extends CustomResource> T getCustomResource(Class<T> type, String name) { | ||
return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get(); | ||
} | ||
|
||
public void register(ResourceController<? extends CustomResource<?, ?>> controller) { | ||
register(controller, null); | ||
} | ||
|
||
@SuppressWarnings({"unchecked", "rawtypes"}) | ||
public void register(ResourceController controller, Retry retry) { | ||
final var config = configurationService.getConfigurationFor(controller); | ||
final var oconfig = override(config).settingNamespace(namespace); | ||
final var path = "/META-INF/fabric8/" + config.getCRDName() + "-v1.yml"; | ||
|
||
if (retry != null) { | ||
oconfig.withRetry(retry); | ||
} | ||
|
||
try (InputStream is = getClass().getResourceAsStream(path)) { | ||
kubernetesClient.load(is).createOrReplace(); | ||
} catch (IOException ex) { | ||
throw new IllegalStateException("Cannot find yaml on classpath: " + path); | ||
} | ||
|
||
if (controller instanceof KubernetesClientAware) { | ||
((KubernetesClientAware) controller).setKubernetesClient(kubernetesClient); | ||
} | ||
|
||
this.operator.register(controller, oconfig.build()); | ||
|
||
LOGGER.info("Controller {} is registered", controller.getClass().getCanonicalName()); | ||
} | ||
|
||
protected void before(ExtensionContext context) { | ||
LOGGER.info("Initializing integration test in namespace {}", namespace); | ||
|
||
kubernetesClient | ||
.namespaces() | ||
.create(new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build()); | ||
|
||
this.operator.start(); | ||
} | ||
|
||
protected void after(ExtensionContext context) { | ||
if (preserveNamespaceOnError && context.getExecutionException().isPresent()) { | ||
LOGGER.info("Preserving namespace {}", namespace); | ||
} else { | ||
LOGGER.info("Deleting namespace {} and stopping operator", namespace); | ||
kubernetesClient.namespaces().withName(namespace).delete(); | ||
Awaitility.await("namespace deleted") | ||
.atMost(45, TimeUnit.SECONDS) | ||
.until(() -> kubernetesClient.namespaces().withName(namespace).get() == null); | ||
} | ||
|
||
try { | ||
this.operator.close(); | ||
} catch (Exception e) { | ||
// ignored | ||
} | ||
try { | ||
this.kubernetesClient.close(); | ||
} catch (Exception e) { | ||
// ignored | ||
} | ||
} | ||
|
||
public static Builder builder() { | ||
return new Builder(); | ||
} | ||
|
||
@SuppressWarnings("rawtypes") | ||
public static class Builder { | ||
private final List<ResourceController> controllers; | ||
private ConfigurationService configurationService; | ||
private boolean preserveNamespaceOnError = false; | ||
|
||
protected Builder() { | ||
this.configurationService = new BaseConfigurationService(Version.UNKNOWN); | ||
this.controllers = new ArrayList<>(); | ||
} | ||
|
||
public Builder preserveNamespaceOnError(boolean value) { | ||
this.preserveNamespaceOnError = value; | ||
return this; | ||
} | ||
|
||
public Builder withConfigurationService(ConfigurationService value) { | ||
configurationService = value; | ||
return this; | ||
} | ||
|
||
@SuppressWarnings("rawtypes") | ||
public Builder withController(ResourceController value) { | ||
controllers.add(value); | ||
return this; | ||
} | ||
|
||
@SuppressWarnings("rawtypes") | ||
public Builder withController(Class<? extends ResourceController> value) { | ||
try { | ||
controllers.add(value.getConstructor().newInstance()); | ||
} catch (Exception e) { | ||
throw new RuntimeException(e); | ||
} | ||
return this; | ||
} | ||
|
||
@SuppressWarnings({"rawtypes", "unchecked"}) | ||
public OperatorExtension build() { | ||
OperatorExtension answer = | ||
new OperatorExtension(configurationService, preserveNamespaceOnError); | ||
for (ResourceController controller : controllers) { | ||
answer.register(controller); | ||
} | ||
|
||
return answer; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I plan on making this class more widely used, cf #496