Skip to content

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

Closed
wants to merge 4 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Collaborator

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

public final ResourceController controller;
public final ControllerConfiguration configuration;

public ControllerRef(ResourceController controller, ControllerConfiguration configuration) {
this.controller = controller;
this.configuration = configuration;
}

public ResourceController getController() {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a leftover when using the ResourceController as part of some stream operation so getter/setter where require for method references

return controller;
}

public ControllerConfiguration getConfiguration() {
return configuration;
}
}
}
Original file line number Diff line number Diff line change
@@ -10,15 +10,15 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AbstractConfigurationService implements ConfigurationService {
public class BaseConfigurationService implements ConfigurationService {

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

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

public AbstractConfigurationService(Version version) {
public BaseConfigurationService(Version version) {
this.version = version;
}

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

@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public <R extends CustomResource> ControllerConfiguration<R> getConfigurationFor(
ResourceController<R> controller) {
Original file line number Diff line number Diff line change
@@ -37,10 +37,14 @@ public static Version loadFromProperties() {

Date builtTime;
try {
builtTime =
// RFC 822 date is the default format used by git-commit-id-plugin
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.parse(properties.getProperty("git.build.time"));
String time = properties.getProperty("git.build.time");
if (time != null) {
builtTime =
// RFC 822 date is the default format used by git-commit-id-plugin
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(time);
} else {
builtTime = Date.from(Instant.EPOCH);
}
} catch (ParseException e) {
builtTime = Date.from(Instant.EPOCH);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package io.javaoperatorsdk.operator.api.config;

import java.time.Instant;
import java.util.Date;

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

public static final Version UNKNOWN = new Version("unknown", "unknown", Date.from(Instant.EPOCH));

private final String sdk;
private final String commit;
private final Date builtTime;
46 changes: 46 additions & 0 deletions operator-framework-junit5/pom.xml
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;
}
}
}
Loading