diff --git a/CHANGELOG.md b/CHANGELOG.md index b80fbae4bb5..87f39987f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/junit/kubernetes-junit-jupiter/pom.xml b/junit/kubernetes-junit-jupiter/pom.xml index 384df902025..06ce0f1af8f 100644 --- a/junit/kubernetes-junit-jupiter/pom.xml +++ b/junit/kubernetes-junit-jupiter/pom.xml @@ -55,5 +55,10 @@ junit-jupiter-api provided + + org.assertj + assertj-core + test + diff --git a/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/BaseExtension.java b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/BaseExtension.java new file mode 100644 index 00000000000..8c5afaa4e9e --- /dev/null +++ b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/BaseExtension.java @@ -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... predicates) { + final List 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 extractFields(Class classWhereFieldIs, Class fieldType, + Predicate... predicates) { + if (classWhereFieldIs != null && classWhereFieldIs != Object.class) { + Stream fieldStream = Arrays.stream(classWhereFieldIs.getDeclaredFields()) + .filter(f -> fieldType.isAssignableFrom(f.getType())); + for (Predicate 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 findAnnotation(Class clazz, Class annotation) { + if (clazz != null) { + if (clazz.isAnnotationPresent(annotation)) { + return clazz.getAnnotation(annotation); + } else if (clazz.getSuperclass() != null) { + return findAnnotation(clazz.getSuperclass(), annotation); + } + } + return null; + } + +} diff --git a/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/HasKubernetesClient.java b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/HasKubernetesClient.java new file mode 100644 index 00000000000..88b0091015b --- /dev/null +++ b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/HasKubernetesClient.java @@ -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; + } +} diff --git a/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/KubernetesNamespacedTestExtension.java b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/KubernetesNamespacedTestExtension.java index 07b06d6efe8..24931bcd06c 100644 --- a/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/KubernetesNamespacedTestExtension.java +++ b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/KubernetesNamespacedTestExtension.java @@ -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; @@ -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) field.getType())); } for (Field field : extractFields(context, Namespace.class, f -> Modifier.isStatic(f.getModifiers()))) { - setFieldValue(field, null, getNamespace(context)); + setFieldValue(field, null, getKubernetesNamespace(context)); } } @@ -59,29 +61,20 @@ public void beforeEach(ExtensionContext context) throws Exception { setFieldValue(field, context.getRequiredTestInstance(), getClient(context).adapt((Class) 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(); } /** @@ -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... predicates) { - final Class testClass = context.getTestClass().orElse(null); - if (testClass != null) { - Stream fieldStream = Arrays.stream(testClass.getDeclaredFields()) - .filter(f -> clazz.isAssignableFrom(f.getType())); - for (Predicate 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); - } } diff --git a/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/LoadKubernetesManifestsExtension.java b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/LoadKubernetesManifestsExtension.java index 93903b5ecbc..a87e84bbb60 100644 --- a/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/LoadKubernetesManifestsExtension.java +++ b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/LoadKubernetesManifestsExtension.java @@ -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) { diff --git a/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/api/KubernetesTest.java b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/api/KubernetesTest.java index 415a21523c8..0805c096a64 100644 --- a/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/api/KubernetesTest.java +++ b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/api/KubernetesTest.java @@ -31,9 +31,13 @@ * Enables and configures the {@link KubernetesNamespacedTestExtension} extension. * *

- * Creates a namespace and configures a {@link KubernetesClient} instance that will + * Creates a {@link KubernetesClient} instance that will * be automatically injected into tests. * + *

+ * Optionally, creates a Namespace for the tests and configures the client to use it. The Namespace + * is deleted after the test suite execution. + * *

{@code
  * @KubernetesTest
  * class MyTest {
@@ -46,4 +50,8 @@
 @Retention(RUNTIME)
 @ExtendWith(KubernetesNamespacedTestExtension.class)
 public @interface KubernetesTest {
+  /**
+   * Create an ephemeral Namespace for the test.
+   */
+  boolean createEphemeralNamespace() default true;
 }
diff --git a/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/api/LoadKubernetesManifests.java b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/api/LoadKubernetesManifests.java
index f2df7ac5452..cf067c143e6 100644
--- a/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/api/LoadKubernetesManifests.java
+++ b/junit/kubernetes-junit-jupiter/src/main/java/io/fabric8/junit/jupiter/api/LoadKubernetesManifests.java
@@ -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();
diff --git a/junit/kubernetes-junit-jupiter/src/test/java/io/fabric8/junit/jupiter/api/KubernetesTestTest.java b/junit/kubernetes-junit-jupiter/src/test/java/io/fabric8/junit/jupiter/api/KubernetesTestTest.java
new file mode 100644
index 00000000000..293ebdd3dc9
--- /dev/null
+++ b/junit/kubernetes-junit-jupiter/src/test/java/io/fabric8/junit/jupiter/api/KubernetesTestTest.java
@@ -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();
+  }
+
+}
diff --git a/junit/kubernetes-junit-jupiter/src/test/java/io/fabric8/junit/jupiter/api/KubernetesTestWithSuperClass.java b/junit/kubernetes-junit-jupiter/src/test/java/io/fabric8/junit/jupiter/api/KubernetesTestWithSuperClass.java
new file mode 100644
index 00000000000..c80b7efe38f
--- /dev/null
+++ b/junit/kubernetes-junit-jupiter/src/test/java/io/fabric8/junit/jupiter/api/KubernetesTestWithSuperClass.java
@@ -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;
+
+}
diff --git a/junit/kubernetes-junit-jupiter/src/test/java/io/fabric8/junit/jupiter/api/KubernetesTestWithSuperClassTest.java b/junit/kubernetes-junit-jupiter/src/test/java/io/fabric8/junit/jupiter/api/KubernetesTestWithSuperClassTest.java
new file mode 100644
index 00000000000..03127e7c632
--- /dev/null
+++ b/junit/kubernetes-junit-jupiter/src/test/java/io/fabric8/junit/jupiter/api/KubernetesTestWithSuperClassTest.java
@@ -0,0 +1,48 @@
+/**
+ * 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 org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class KubernetesTestWithSuperClassTest extends KubernetesTestWithSuperClass {
+
+  private static KubernetesClient staticClient;
+  private KubernetesClient client;
+
+  @Test
+  void staticSuperClientIsSet() {
+    assertThat(staticSuperClient).isNotNull();
+  }
+
+  @Test
+  void superClientIsSet() {
+    assertThat(superClient).isNotNull();
+  }
+
+  @Test
+  void staticClientIsSet() {
+    assertThat(staticClient).isNotNull();
+  }
+
+  @Test
+  void clientIsSet() {
+    assertThat(client).isNotNull();
+  }
+
+}