Skip to content
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

feat: @KubernetesTest annotation can be used in base test classes #4734

Merged
merged 2 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* Fix #4670: the initial informer listing will use a resourceVersion of 0 to utilize the watch cache if possible. This means that the initial cache state when the informer is returned, or the start future is completed, may not be as fresh as the previous behavior which forced the latest version. It will of course become more consistent as the watch will already have been established.
* Fix #4694: [java-generator] Option to override the package name of the generated code.
* Fix #4720: interceptors close any response body if the response is not a 2xx response.
* Fix #4734: @KubernetesTest annotation can be used in base test classes
* Fix #4734: @KubernetesTest creates an ephemeral Namespace optionally (can opt-out)

#### Dependency Upgrade

Expand Down
5 changes: 5 additions & 0 deletions junit/kubernetes-junit-jupiter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,10 @@
<artifactId>junit-jupiter-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.junit.jupiter;

import org.junit.jupiter.api.extension.ExtensionContext;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public interface BaseExtension {

default ExtensionContext.Namespace getNamespace(ExtensionContext context) {
return ExtensionContext.Namespace.create(context.getRequiredTestClass());
}

default ExtensionContext.Store getStore(ExtensionContext context) {
return context.getRoot().getStore(getNamespace(context));
}

default Field[] extractFields(ExtensionContext context, Class<?> clazz, Predicate<Field>... predicates) {
final List<Field> fields = new ArrayList<>();
Class<?> testClass = context.getTestClass().orElse(Object.class);
do {
fields.addAll(extractFields(testClass, clazz, predicates));
testClass = testClass.getSuperclass();
} while (testClass != Object.class);
return fields.toArray(new Field[0]);
}

/* private */static List<Field> extractFields(Class<?> classWhereFieldIs, Class<?> fieldType,
Predicate<Field>... predicates) {
if (classWhereFieldIs != null && classWhereFieldIs != Object.class) {
Stream<Field> fieldStream = Arrays.stream(classWhereFieldIs.getDeclaredFields())
.filter(f -> fieldType.isAssignableFrom(f.getType()));
for (Predicate<Field> p : predicates) {
fieldStream = fieldStream.filter(p);
}
return fieldStream.collect(Collectors.toList());
}
return Collections.emptyList();
}

default void setFieldValue(Field field, Object entity, Object value) throws IllegalAccessException {
final boolean isAccessible = field.isAccessible();
field.setAccessible(true);
field.set(entity, value);
field.setAccessible(isAccessible);
}

default <T extends Annotation> T findAnnotation(Class<?> clazz, Class<T> annotation) {
if (clazz != null) {
if (clazz.isAnnotationPresent(annotation)) {
return clazz.getAnnotation(annotation);
} else if (clazz.getSuperclass() != null) {
return findAnnotation(clazz.getSuperclass(), annotation);
}
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.junit.jupiter;

import io.fabric8.kubernetes.client.KubernetesClient;
import org.junit.jupiter.api.extension.ExtensionContext;

public interface HasKubernetesClient extends BaseExtension {

default KubernetesClient getClient(ExtensionContext context) {
final KubernetesClient client = getStore(context).get(KubernetesClient.class, KubernetesClient.class);
if (client == null) {
throw new IllegalStateException("No KubernetesClient found");
}
return client;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.fabric8.junit.jupiter;

import io.fabric8.junit.jupiter.api.KubernetesTest;
import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
import io.fabric8.kubernetes.api.model.ObjectReference;
Expand All @@ -29,27 +30,28 @@

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Stream;

public class KubernetesNamespacedTestExtension implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback {
public class KubernetesNamespacedTestExtension
implements HasKubernetesClient, BeforeAllCallback, BeforeEachCallback, AfterAllCallback {

@Override
public void beforeAll(ExtensionContext context) throws Exception {
final KubernetesClient client = new KubernetesClientBuilder().build();
getStore(context).put(Namespace.class, initNamespace(client));
getStore(context).put(KubernetesClient.class,
client.adapt(NamespacedKubernetesClient.class).inNamespace(getNamespace(context).getMetadata().getName()));
getStore(context).put(KubernetesClient.class, client);
if (shouldCreateNamespace(context)) {
getStore(context).put(Namespace.class, initNamespace(client));
getStore(context).put(KubernetesClient.class,
client.adapt(NamespacedKubernetesClient.class).inNamespace(getKubernetesNamespace(context).getMetadata().getName()));
}
for (Field field : extractFields(context, KubernetesClient.class, f -> Modifier.isStatic(f.getModifiers()))) {
setFieldValue(field, null, getClient(context).adapt((Class<Client>) field.getType()));
}
for (Field field : extractFields(context, Namespace.class, f -> Modifier.isStatic(f.getModifiers()))) {
setFieldValue(field, null, getNamespace(context));
setFieldValue(field, null, getKubernetesNamespace(context));
}
}

Expand All @@ -59,29 +61,20 @@ public void beforeEach(ExtensionContext context) throws Exception {
setFieldValue(field, context.getRequiredTestInstance(), getClient(context).adapt((Class<Client>) field.getType()));
}
for (Field field : extractFields(context, Namespace.class, f -> !Modifier.isStatic(f.getModifiers()))) {
setFieldValue(field, context.getRequiredTestInstance(), getNamespace(context));
setFieldValue(field, context.getRequiredTestInstance(), getKubernetesNamespace(context));
}
}

@Override
public void afterAll(ExtensionContext context) {
final KubernetesClient client = getClient(context);
client.resource(getNamespace(context)).withGracePeriod(0L).delete();
client.close();
}

static KubernetesClient getClient(ExtensionContext context) {
final KubernetesClient client = getStore(context).get(KubernetesClient.class, KubernetesClient.class);
if (client == null) {
throw new IllegalStateException("No KubernetesClient found");
if (shouldCreateNamespace(context)) {
client.resource(getKubernetesNamespace(context)).withGracePeriod(0L).delete();
}
return client;
}

private static ExtensionContext.Store getStore(ExtensionContext context) {
ExtensionContext.Namespace namespace = ExtensionContext.Namespace.create(KubernetesNamespacedTestExtension.class,
context.getRequiredTestClass());
return context.getRoot().getStore(namespace);
// Note that the ThreadPoolExecutor in OkHttp's RealConnectionPool is shared amongst all the OkHttp client
// instances. This means that closing one OkHttp client instance effectively closes all the others.
// In order to be able to use this safely, we should transition to one of the other HttpClient implementations
client.close();
}

/**
Expand Down Expand Up @@ -120,31 +113,16 @@ private static Namespace initNamespace(KubernetesClient client) {
return namespace;
}

private static Namespace getNamespace(ExtensionContext context) {
private boolean shouldCreateNamespace(ExtensionContext context) {
final KubernetesTest annotation = findAnnotation(context.getRequiredTestClass(), KubernetesTest.class);
return annotation == null || annotation.createEphemeralNamespace();
}

private Namespace getKubernetesNamespace(ExtensionContext context) {
final Namespace namespace = getStore(context).get(Namespace.class, Namespace.class);
if (namespace == null) {
throw new IllegalStateException("No Kubernetes Namespace found");
}
return namespace;
}

private static Field[] extractFields(ExtensionContext context, Class<?> clazz, Predicate<Field>... predicates) {
final Class<?> testClass = context.getTestClass().orElse(null);
if (testClass != null) {
Stream<Field> fieldStream = Arrays.stream(testClass.getDeclaredFields())
.filter(f -> clazz.isAssignableFrom(f.getType()));
for (Predicate<Field> p : predicates) {
fieldStream = fieldStream.filter(p);
}
return fieldStream.toArray(Field[]::new);
}
return new Field[0];
}

private static void setFieldValue(Field field, Object entity, Object value) throws IllegalAccessException {
final boolean isAccessible = field.isAccessible();
field.setAccessible(true);
field.set(entity, value);
field.setAccessible(isAccessible);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

import static io.fabric8.junit.jupiter.KubernetesNamespacedTestExtension.getClient;

public class LoadKubernetesManifestsExtension implements BeforeAllCallback, AfterAllCallback {
/**
* Must be used in conjunction with {@link KubernetesNamespacedTestExtension} to be able to consume a KubernetesClient
*/
public class LoadKubernetesManifestsExtension implements HasKubernetesClient, BeforeAllCallback, AfterAllCallback {

@Override
public void beforeAll(ExtensionContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@
* Enables and configures the {@link KubernetesNamespacedTestExtension} extension.
*
* <p>
* Creates a namespace and configures a {@link KubernetesClient} instance that will
* Creates a {@link KubernetesClient} instance that will
* be automatically injected into tests.
*
* <p>
* Optionally, creates a Namespace for the tests and configures the client to use it. The Namespace
* is deleted after the test suite execution.
*
* <pre>{@code
* &#64;KubernetesTest
* class MyTest {
Expand All @@ -46,4 +50,8 @@
@Retention(RUNTIME)
@ExtendWith(KubernetesNamespacedTestExtension.class)
public @interface KubernetesTest {
/**
* Create an ephemeral Namespace for the test.
*/
boolean createEphemeralNamespace() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
*/
@Target({ TYPE, METHOD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@ExtendWith({ KubernetesNamespacedTestExtension.class, LoadKubernetesManifestsExtension.class })
@ExtendWith(KubernetesNamespacedTestExtension.class)
@ExtendWith(LoadKubernetesManifestsExtension.class)
public @interface LoadKubernetesManifests {

String[] value();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.junit.jupiter.api;

import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.NamespacedKubernetesClient;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

@KubernetesTest(createEphemeralNamespace = false)
// Due to the way the extensions interact with the test, this test class can't be parameterized
class KubernetesTestTest {

private static KubernetesClient staticKubernetesClient;
private KubernetesClient kubernetesClient;
private static NamespacedKubernetesClient staticNamespacedKubernetesClient;
private NamespacedKubernetesClient namespacedKubernetesClient;

@Test
void staticKubernetesClient() {
assertThat(staticKubernetesClient).isNotNull();
}

@Test
void kubernetesClient() {
assertThat(kubernetesClient).isNotNull();
}

@Test
void staticNamespacedKubernetesClient() {
assertThat(staticNamespacedKubernetesClient).isNotNull();
}

@Test
void namespacedKubernetesClient() {
assertThat(namespacedKubernetesClient).isNotNull();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.junit.jupiter.api;

import io.fabric8.kubernetes.client.KubernetesClient;

@KubernetesTest(createEphemeralNamespace = false)
public class KubernetesTestWithSuperClass {

static KubernetesClient staticSuperClient;
KubernetesClient superClient;

}
Loading