From cd4f45846a3d24c6d8da584c4508155f61236dc5 Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Thu, 4 Feb 2021 14:21:28 +0100 Subject: [PATCH 01/14] Fix for #14744: support KibernetesServer test resource for CRUD operations --- .../AbstractKubernetesTestResource.java | 83 +++++++++++++++++++ .../KubernetesMockServerTestResource.java | 83 +++++++------------ .../client/KubernetesServerTestResource.java | 42 ++++++++++ .../test/kubernetes/client/Server.java | 18 ++++ 4 files changed, 175 insertions(+), 51 deletions(-) create mode 100644 test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/AbstractKubernetesTestResource.java create mode 100644 test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java create mode 100644 test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/Server.java diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/AbstractKubernetesTestResource.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/AbstractKubernetesTestResource.java new file mode 100644 index 0000000000000..d8be65e175cea --- /dev/null +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/AbstractKubernetesTestResource.java @@ -0,0 +1,83 @@ +package io.quarkus.test.kubernetes.client; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.GenericKubernetesClient; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public abstract class AbstractKubernetesTestResource implements QuarkusTestResourceLifecycleManager { + protected T server; + + @Override + public Map start() { + final Map systemProps = new HashMap<>(); + systemProps.put(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + systemProps.put(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + systemProps.put(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + systemProps.put(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + systemProps.put(Config.KUBERNETES_HTTP2_DISABLE, "true"); + + server = createServer(); + initServer(); + + try (GenericKubernetesClient client = getClient()) { + systemProps.put(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, client.getConfiguration().getMasterUrl()); + } + + configureServer(); + + return systemProps; + } + + protected abstract GenericKubernetesClient getClient(); + + /** + * Can be used by subclasses in order to + * setup the mock server before the Quarkus application starts + */ + protected void configureServer() { + } + + protected void initServer() { + } + + protected abstract T createServer(); + + protected boolean useHttps() { + return Boolean.getBoolean("quarkus.kubernetes-client.test.https"); + } + + @Override + public void inject(Object testInstance) { + Class c = testInstance.getClass(); + Class annotation = getInjectionAnnotation(); + Class injectedClass = getInjectedClass(); + while (c != Object.class) { + for (Field f : c.getDeclaredFields()) { + if (f.getAnnotation(annotation) != null) { + if (!injectedClass.isAssignableFrom(f.getType())) { + throw new RuntimeException(annotation + " can only be used on fields of type " + injectedClass); + } + + f.setAccessible(true); + try { + f.set(testInstance, server); + return; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + c = c.getSuperclass(); + } + } + + protected abstract Class getInjectedClass(); + + protected abstract Class getInjectionAnnotation(); + +} diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java index 445979ba56800..b798f26b8622d 100644 --- a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java @@ -1,81 +1,62 @@ package io.quarkus.test.kubernetes.client; -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; +import java.lang.annotation.Annotation; -import io.fabric8.kubernetes.client.Config; -import io.fabric8.kubernetes.client.NamespacedKubernetesClient; +import io.fabric8.kubernetes.client.GenericKubernetesClient; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; -import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; -public class KubernetesMockServerTestResource implements QuarkusTestResourceLifecycleManager { - - private KubernetesMockServer mockServer; +public class KubernetesMockServerTestResource extends AbstractKubernetesTestResource { @Override - public Map start() { - final Map systemProps = new HashMap<>(); - systemProps.put(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); - systemProps.put(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); - systemProps.put(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); - systemProps.put(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); - systemProps.put(Config.KUBERNETES_HTTP2_DISABLE, "true"); - - mockServer = createMockServer(); - mockServer.init(); - try (NamespacedKubernetesClient client = mockServer.createClient()) { - systemProps.put(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, client.getConfiguration().getMasterUrl()); - } + protected GenericKubernetesClient getClient() { + return server.createClient(); + } - configureMockServer(mockServer); + @Override + protected void initServer() { + server.init(); + } - return systemProps; + @Override + protected KubernetesMockServer createServer() { + return createMockServer(); } + /** + * @deprecated use {@link #createServer()} + */ + @Deprecated protected KubernetesMockServer createMockServer() { return new KubernetesMockServer(useHttps()); } + @Override + protected void configureServer() { + configureMockServer(server); + } + /** - * Can be used by subclasses of {@code KubernetesMockServerTestResource} in order to - * setup the mock server before the Quarkus application starts + * @deprecated use {@link #configureServer()} */ + @Deprecated public void configureMockServer(KubernetesMockServer mockServer) { - } @Override public void stop() { - if (mockServer != null) { - mockServer.destroy(); + if (server != null) { + server.destroy(); + server = null; } } @Override - public void inject(Object testInstance) { - Class c = testInstance.getClass(); - while (c != Object.class) { - for (Field f : c.getDeclaredFields()) { - if (f.getAnnotation(MockServer.class) != null) { - if (!KubernetesMockServer.class.isAssignableFrom(f.getType())) { - throw new RuntimeException("@MockServer can only be used on fields of type KubernetesMockServer"); - } - - f.setAccessible(true); - try { - f.set(testInstance, mockServer); - return; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - c = c.getSuperclass(); - } + protected Class getInjectedClass() { + return KubernetesMockServer.class; } - protected boolean useHttps() { - return Boolean.getBoolean("quarkus.kubernetes-client.test.https"); + @Override + protected Class getInjectionAnnotation() { + return MockServer.class; } } diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java new file mode 100644 index 0000000000000..e94743530f80a --- /dev/null +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java @@ -0,0 +1,42 @@ +package io.quarkus.test.kubernetes.client; + +import java.lang.annotation.Annotation; + +import io.fabric8.kubernetes.client.GenericKubernetesClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; + +public class KubernetesServerTestResource extends AbstractKubernetesTestResource { + + @Override + protected GenericKubernetesClient getClient() { + return server.getClient(); + } + + @Override + protected void initServer() { + server.before(); + } + + @Override + protected KubernetesServer createServer() { + return new KubernetesServer(useHttps(), true); + } + + @Override + public void stop() { + if (server != null) { + server.after(); + server = null; + } + } + + @Override + protected Class getInjectedClass() { + return KubernetesServer.class; + } + + @Override + protected Class getInjectionAnnotation() { + return Server.class; + } +} diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/Server.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/Server.java new file mode 100644 index 0000000000000..2cdb47708d0fd --- /dev/null +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/Server.java @@ -0,0 +1,18 @@ +package io.quarkus.test.kubernetes.client; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; + +/** + * Used to specify that the field should be injected with the mock Kubernetes API server + * Can only be used on type {@link KubernetesServer} + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Server { + +} From e4b231e12cc78c33f1f140e4ff5ea518660648a5 Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Mon, 8 Feb 2021 14:35:12 +0100 Subject: [PATCH 02/14] Kubernetes client test: fix what is produces to map to what the test expects --- .../src/main/java/io/quarkus/it/kubernetes/client/Pods.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/Pods.java b/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/Pods.java index 7c82dedde42a1..610ebbbfa63ad 100644 --- a/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/Pods.java +++ b/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/Pods.java @@ -52,7 +52,8 @@ public Response updateFirst(@PathParam("namespace") String namespace) { final Pod pod = pods.get(0); final String podName = pod.getMetadata().getName(); // would normally do some kind of meaningful update here - Pod updatedPod = new PodBuilder().withNewMetadata().withName(podName).withNewResourceVersion("12345").endMetadata() + Pod updatedPod = new PodBuilder().withNewMetadata().withName(podName).withNewResourceVersion("12345") + .addToLabels("key1", "value1").endMetadata() .build(); updatedPod = kubernetesClient.pods().withName(podName).createOrReplace(updatedPod); From c0c38b1247f3af7d0a1c6e5c26c3faa91472385d Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Mon, 8 Feb 2021 14:36:43 +0100 Subject: [PATCH 03/14] KubernetesTestServer supports configuration now --- ...ustomKubernetesTestServerTestResource.java | 65 +++++++++++++++++++ .../client/KubernetesNewClientTest.java | 52 +++++++++++++++ .../client/KubernetesTestServerTest.java | 59 +++++++++++++++++ .../KubernetesMockServerTestResource.java | 4 ++ .../client/KubernetesServerTestResource.java | 40 +++++++++++- ...{Server.java => KubernetesTestServer.java} | 2 +- 6 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesTestServerTestResource.java create mode 100644 integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesNewClientTest.java create mode 100644 integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java rename test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/{Server.java => KubernetesTestServer.java} (92%) diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesTestServerTestResource.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesTestServerTestResource.java new file mode 100644 index 0000000000000..a28944ab803c4 --- /dev/null +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/CustomKubernetesTestServerTestResource.java @@ -0,0 +1,65 @@ +package io.quarkus.it.kubernetes.client; + +import java.util.Base64; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.quarkus.test.kubernetes.client.KubernetesServerTestResource; + +public class CustomKubernetesTestServerTestResource extends KubernetesServerTestResource { + + // setup the ConfigMap objects that the application expects to lookup configuration from + @Override + protected void configureServer() { + server.getClient().inNamespace("test").configMaps().create(configMapBuilder("cmap1") + .addToData("dummy", "dummy") + .addToData("overridden.secret", "cm") // will be overridden since secrets have a higher priority + .addToData("some.prop1", "val1") + .addToData("some.prop2", "val2") + .addToData("some.prop4", "v4") // will be overridden since cmap2 has a higher priority + .addToData("some.prop5", "val5") + .addToData("application.properties", "some.prop3=val3") + .addToData("application.yaml", "some:\n prop4: val4").build()); + + server.getClient().inNamespace("test").configMaps().create(configMapBuilder("cmap2") + .addToData("application.yaml", "some:\n prop4: val4").build()); + + server.getClient().inNamespace("test").configMaps().create(configMapBuilder("cmap3") + .addToData("dummy", "dummyFromDemo") + .addToData("some.prop1", "val1FromDemo") + .addToData("some.prop2", "val2FromDemo") + .addToData("some.prop5", "val5FromDemo") + .addToData("application.properties", "some.prop3=val3FromDemo") + .addToData("application.yaml", "some:\n prop4: val4FromDemo").build()); + + server.getClient().inNamespace("test").secrets().create(secretBuilder("s1") + .addToData("dummysecret", encodeValue("dummysecret")) + .addToData("overridden.secret", encodeValue("secret")) + .addToData("secret.prop1", encodeValue("val1")) + .addToData("secret.prop2", encodeValue("val2")) + .addToData("application.properties", encodeValue("secret.prop3=val3")) + .addToData("application.yaml", encodeValue("secret:\n prop4: val4")).build()); + + server.getClient().inNamespace("test").secrets().create(secretBuilder("s1") + .addToData("dummysecret", encodeValue("dummysecretFromDemo")) + .addToData("overridden.secret", encodeValue("secretFromDemo")) + .addToData("secret.prop1", encodeValue("val1FromDemo")) + .addToData("secret.prop2", encodeValue("val2FromDemo")) + .addToData("application.properties", encodeValue("secret.prop3=val3FromDemo")) + .addToData("application.yaml", encodeValue("secret:\n prop4: val4FromDemo")).build()); + } + + private ConfigMapBuilder configMapBuilder(String name) { + return new ConfigMapBuilder().withNewMetadata() + .withName(name).endMetadata(); + } + + private SecretBuilder secretBuilder(String name) { + return new SecretBuilder().withNewMetadata() + .withName(name).endMetadata(); + } + + private String encodeValue(String value) { + return Base64.getEncoder().encodeToString(value.getBytes()); + } +} diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesNewClientTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesNewClientTest.java new file mode 100644 index 0000000000000..7a82130dd121f --- /dev/null +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesNewClientTest.java @@ -0,0 +1,52 @@ +package io.quarkus.it.kubernetes.client; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.kubernetes.client.KubernetesTestServer; +import io.restassured.RestAssured; + +/* + * KubernetesClientTest.TestResource contains the entire process of setting up the Mock Kubernetes API Server + * It has to live there otherwise the Kubernetes client in native mode won't be able to locate the mock API Server + */ +@QuarkusTestResource(CustomKubernetesTestServerTestResource.class) +@QuarkusTest +public class KubernetesNewClientTest { + + @KubernetesTestServer + private KubernetesServer mockServer; + + @Test + public void testInteractionWithAPIServer() throws InterruptedException { + setupMockServerForTest(); + + RestAssured.when().get("/pod/test").then() + .body("size()", is(2)).body(containsString("pod1"), containsString("pod2")); + + RestAssured.when().delete("/pod/test").then() + .statusCode(204); + + RestAssured.when().put("/pod/test").then() + .body(containsString("value1")); + + RestAssured.when().post("/pod/test").then() + .body(containsString("12345")); + } + + private void setupMockServerForTest() { + Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); + Pod pod2 = new PodBuilder().withNewMetadata().withName("pod2").withNamespace("test").and().build(); + + mockServer.getClient().inNamespace("test").pods().create(pod1); + mockServer.getClient().inNamespace("test").pods().create(pod2); + } + +} diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java new file mode 100644 index 0000000000000..16c043c9177c3 --- /dev/null +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java @@ -0,0 +1,59 @@ +package io.quarkus.it.kubernetes.client; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.kubernetes.client.KubernetesServerTestResource; +import io.quarkus.test.kubernetes.client.KubernetesTestServer; + +@TestProfile(KubernetesTestServerTest.MyProfile.class) +@QuarkusTestResource(value = KubernetesServerTestResource.class, initArgs = { + @ResourceArg(name = KubernetesServerTestResource.HTTPS, value = "false"), + @ResourceArg(name = KubernetesServerTestResource.CRUD, value = "false"), + @ResourceArg(name = KubernetesServerTestResource.PORT, value = "10001"), +}) +@QuarkusTest +public class KubernetesTestServerTest { + + @KubernetesTestServer + private KubernetesServer mockServer; + + @Test + public void testConfiguration() throws InterruptedException { + // we can't really test CRUD, and HTTPS doesn't work + Assertions.assertEquals(mockServer.getMockServer().getPort(), 10001); + } + + public static class MyProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + Map overrides = new HashMap<>(); + // do not fetch config from kubernetes + overrides.put("quarkus.kubernetes-config.enabled", "false"); + overrides.put("quarkus.kubernetes-config.secrets.enabled", "false"); + // get rid of errors due to us not populating config from kubernetes + overrides.put("dummy", "asd"); + overrides.put("some.prop1", "asd"); + overrides.put("some.prop2", "asd"); + overrides.put("some.prop3", "asd"); + overrides.put("some.prop4", "asd"); + overrides.put("some.prop5", "asd"); + overrides.put("secret.prop1", "asd"); + overrides.put("secret.prop2", "asd"); + overrides.put("secret.prop3", "asd"); + overrides.put("secret.prop4", "asd"); + overrides.put("overridden.secret", "asd"); + overrides.put("dummysecret", "asd"); + return overrides; + } + } +} diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java index b798f26b8622d..94102685abe58 100644 --- a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesMockServerTestResource.java @@ -5,6 +5,10 @@ import io.fabric8.kubernetes.client.GenericKubernetesClient; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +/** + * @deprecated use {@link KubernetesServerTestResource} + */ +@Deprecated public class KubernetesMockServerTestResource extends AbstractKubernetesTestResource { @Override diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java index e94743530f80a..be62d76be9826 100644 --- a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java @@ -1,12 +1,48 @@ package io.quarkus.test.kubernetes.client; import java.lang.annotation.Annotation; +import java.net.InetAddress; +import java.util.Collections; +import java.util.Map; import io.fabric8.kubernetes.client.GenericKubernetesClient; import io.fabric8.kubernetes.client.server.mock.KubernetesServer; public class KubernetesServerTestResource extends AbstractKubernetesTestResource { + /** + * Configure HTTPS usage, defaults to false + */ + public final static String HTTPS = "https"; + /** + * Configure CRUD usage, defaults to true + */ + public final static String CRUD = "crud"; + /** + * Configure the port to use, defaults to 0, for the first available port + */ + public final static String PORT = "port"; + + private boolean https = false; + private boolean crud = true; + private int port = 0; + + @Override + public void init(Map initArgs) { + String val = initArgs.get(HTTPS); + if (val != null) { + this.https = Boolean.parseBoolean(val); + } + val = initArgs.get(CRUD); + if (val != null) { + this.crud = Boolean.parseBoolean(val); + } + val = initArgs.get(PORT); + if (val != null) { + this.port = Integer.parseInt(val); + } + } + @Override protected GenericKubernetesClient getClient() { return server.getClient(); @@ -19,7 +55,7 @@ protected void initServer() { @Override protected KubernetesServer createServer() { - return new KubernetesServer(useHttps(), true); + return new KubernetesServer(https, crud, InetAddress.getLoopbackAddress(), port, Collections.emptyList()); } @Override @@ -37,6 +73,6 @@ protected Class getInjectedClass() { @Override protected Class getInjectionAnnotation() { - return Server.class; + return KubernetesTestServer.class; } } diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/Server.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesTestServer.java similarity index 92% rename from test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/Server.java rename to test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesTestServer.java index 2cdb47708d0fd..bb78b5413ad6a 100644 --- a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/Server.java +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesTestServer.java @@ -13,6 +13,6 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) -public @interface Server { +public @interface KubernetesTestServer { } From a3e61a977dda1413e7eeac6af446e4e73fd56aa4 Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Mon, 8 Feb 2021 14:35:35 +0100 Subject: [PATCH 04/14] Support meta-annotation-based config of test resources --- ...tResourceConfigurableLifecycleManager.java | 9 +++ .../test/common/TestResourceManager.java | 70 +++++++++++++++++-- .../test/junit/QuarkusTestExtension.java | 5 +- 3 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceConfigurableLifecycleManager.java diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceConfigurableLifecycleManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceConfigurableLifecycleManager.java new file mode 100644 index 0000000000000..9b082629b62be --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceConfigurableLifecycleManager.java @@ -0,0 +1,9 @@ +package io.quarkus.test.common; + +import java.lang.annotation.Annotation; + +public interface QuarkusTestResourceConfigurableLifecycleManager + extends QuarkusTestResourceLifecycleManager { + default void init(ConfigAnnotation annotation) { + } +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java index b388e92bbcce1..101e41a484dc1 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java @@ -1,6 +1,7 @@ package io.quarkus.test.common; import java.io.Closeable; +import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; @@ -23,11 +24,14 @@ import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; public class TestResourceManager implements Closeable { + private static final DotName DOTNAME_QUARKUS_TEST_RESOURCE = DotName.createSimple(QuarkusTestResource.class.getName()); + private final List sequentialTestResourceEntries; private final List parallelTestResourceEntries; private final List allTestResourceEntries; @@ -61,7 +65,14 @@ public TestResourceManager(Class testClass, List addi public void init() { for (TestResourceEntry entry : allTestResourceEntries) { try { - entry.getTestResource().init(entry.getArgs()); + QuarkusTestResourceLifecycleManager testResource = entry.getTestResource(); + if (testResource instanceof QuarkusTestResourceConfigurableLifecycleManager + && entry.getConfigAnnotation() != null) { + ((QuarkusTestResourceConfigurableLifecycleManager) testResource) + .init(entry.getConfigAnnotation()); + } else { + testResource.init(entry.getArgs()); + } } catch (Exception e) { throw new RuntimeException("Unable initialize test resource " + entry.getTestResource(), e); } @@ -227,7 +238,7 @@ public int compare(TestResourceEntry o1, TestResourceEntry o2) { private TestResourceManager.TestResourceEntry buildTestResourceEntry(TestResourceClassEntry entry) { Class testResourceClass = entry.clazz; try { - return new TestResourceEntry(testResourceClass.getConstructor().newInstance(), entry.args); + return new TestResourceEntry(testResourceClass.getConstructor().newInstance(), entry.args, entry.configAnnotation); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException @@ -242,6 +253,40 @@ private Set getUniqueTestResourceClassEntries(Class t List additionalTestResources) { IndexView index = TestClassIndexer.readIndex(testClass); Set uniqueEntries = new HashSet<>(); + // reload the test class in the right CL + Class testClassFromTCCL; + try { + testClassFromTCCL = Class.forName(testClass.getName(), false, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + // handle meta-annotations: in this case we must rely on reflection because meta-annotations are not indexed + // because they are not in the user's test folder but come from test extensions + for (Annotation reflAnnotation : testClassFromTCCL.getAnnotations()) { + for (Annotation annotationAnnotation : reflAnnotation.annotationType().getAnnotations()) { + if (annotationAnnotation.annotationType() == QuarkusTestResource.class) { + QuarkusTestResource testResource = (QuarkusTestResource) annotationAnnotation; + Class testResourceClass = testResource.value(); + + ResourceArg[] argsAnnotationValue = testResource.initArgs(); + Map args; + if (argsAnnotationValue.length == 0) { + args = Collections.emptyMap(); + } else { + args = new HashMap<>(); + for (ResourceArg arg : argsAnnotationValue) { + args.put(arg.name(), arg.value()); + } + } + + boolean isParallel = testResource.parallel(); + + uniqueEntries.add(new TestResourceClassEntry(testResourceClass, args, reflAnnotation, isParallel)); + + break; + } + } + } for (AnnotationInstance annotation : findQuarkusTestResourceInstances(index)) { try { Class testResourceClass = loadTestResourceClassFromTCCL( @@ -265,7 +310,7 @@ private Set getUniqueTestResourceClassEntries(Class t isParallel = parallelAnnotationValue.asBoolean(); } - uniqueEntries.add(new TestResourceClassEntry(testResourceClass, args, isParallel)); + uniqueEntries.add(new TestResourceClassEntry(testResourceClass, args, null, isParallel)); } catch (IllegalArgumentException | SecurityException e) { throw new RuntimeException("Unable to instantiate the test resource " + annotation.value().asString(), e); } @@ -300,11 +345,14 @@ public static class TestResourceClassEntry { private Class clazz; private Map args; private boolean parallel; + private Annotation configAnnotation; public TestResourceClassEntry(Class clazz, Map args, + Annotation configAnnotation, boolean parallel) { this.clazz = clazz; this.args = args; + this.configAnnotation = configAnnotation; this.parallel = parallel; } @@ -315,12 +363,13 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; TestResourceClassEntry that = (TestResourceClassEntry) o; - return clazz.equals(that.clazz) && args.equals(that.args) && parallel == that.parallel; + return clazz.equals(that.clazz) && args.equals(that.args) && Objects.equals(configAnnotation, that.configAnnotation) + && parallel == that.parallel; } @Override public int hashCode() { - return Objects.hash(clazz, args, parallel); + return Objects.hash(clazz, args, configAnnotation, parallel); } public boolean isParallel() { @@ -332,14 +381,17 @@ private static class TestResourceEntry { private final QuarkusTestResourceLifecycleManager testResource; private final Map args; + private final Annotation configAnnotation; public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource) { - this(testResource, Collections.emptyMap()); + this(testResource, Collections.emptyMap(), null); } - public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource, Map args) { + public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource, Map args, + Annotation configAnnotation) { this.testResource = testResource; this.args = args; + this.configAnnotation = configAnnotation; } public QuarkusTestResourceLifecycleManager getTestResource() { @@ -349,6 +401,10 @@ public QuarkusTestResourceLifecycleManager getTestResource() { public Map getArgs() { return args; } + + public Annotation getConfigAnnotation() { + return configAnnotation; + } } } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index f88af54a72e98..5ac275cebb649 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -6,6 +6,7 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; +import java.lang.annotation.Annotation; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.reflect.Constructor; @@ -400,14 +401,14 @@ private List getAdditionalTestResources( try { Constructor testResourceClassEntryConstructor = Class .forName(TestResourceManager.TestResourceClassEntry.class.getName(), true, classLoader) - .getConstructor(Class.class, Map.class, boolean.class); + .getConstructor(Class.class, Map.class, Annotation.class, boolean.class); List testResources = profileInstance.testResources(); List result = new ArrayList<>(testResources.size()); for (QuarkusTestProfile.TestResourceEntry testResource : testResources) { Object instance = testResourceClassEntryConstructor.newInstance( Class.forName(testResource.getClazz().getName(), true, classLoader), testResource.getArgs(), - testResource.isParallel()); + null, testResource.isParallel()); result.add(instance); } From 3b09e5467f61dbeaedfad93a6fcec49167ba9c92 Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Tue, 9 Feb 2021 11:41:54 +0100 Subject: [PATCH 05/14] Added QuarkusTestResource.restrictToAnnotatedTest To force a test resource to be test-specific --- .../test/common/QuarkusTestResource.java | 7 +++++ .../test/common/TestResourceManager.java | 31 ++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResource.java b/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResource.java index f9f01995c7887..a356848b26d83 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResource.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResource.java @@ -42,6 +42,13 @@ */ boolean parallel() default false; + /** + * Whether this annotation should only be enabled if it is placed on the currently running test class. + * Note that this defaults to true for meta-annotations since meta-annotations are only considered + * for the current test class. + */ + boolean restrictToAnnotatedTest() default false; + @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java index 101e41a484dc1..94dc6dc077af4 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java @@ -266,6 +266,9 @@ private Set getUniqueTestResourceClassEntries(Class t for (Annotation annotationAnnotation : reflAnnotation.annotationType().getAnnotations()) { if (annotationAnnotation.annotationType() == QuarkusTestResource.class) { QuarkusTestResource testResource = (QuarkusTestResource) annotationAnnotation; + + // NOTE: we don't need to check restrictToAnnotatedTest because by design config-based annotations + // are not discovered outside the test class, so they're restricted Class testResourceClass = testResource.value(); ResourceArg[] argsAnnotationValue = testResource.initArgs(); @@ -287,7 +290,7 @@ private Set getUniqueTestResourceClassEntries(Class t } } } - for (AnnotationInstance annotation : findQuarkusTestResourceInstances(index)) { + for (AnnotationInstance annotation : findQuarkusTestResourceInstances(testClass, index)) { try { Class testResourceClass = loadTestResourceClassFromTCCL( annotation.value().asString()); @@ -330,16 +333,34 @@ private Class loadTestResourceCla } } - private Collection findQuarkusTestResourceInstances(IndexView index) { - Set testResourceAnnotations = new HashSet<>( - index.getAnnotations(DotName.createSimple(QuarkusTestResource.class.getName()))); + private Collection findQuarkusTestResourceInstances(Class testClass, IndexView index) { + Set testResourceAnnotations = new HashSet<>(); + for (AnnotationInstance annotation : index.getAnnotations(DotName.createSimple(QuarkusTestResource.class.getName()))) { + if (keepTestResourceAnnotation(annotation, annotation.target().asClass(), testClass)) { + testResourceAnnotations.add(annotation); + } + } + for (AnnotationInstance annotation : index .getAnnotations(DotName.createSimple(QuarkusTestResource.List.class.getName()))) { - Collections.addAll(testResourceAnnotations, annotation.value().asNestedArray()); + for (AnnotationInstance nestedAnnotation : annotation.value().asNestedArray()) { + // keep the list target + if (keepTestResourceAnnotation(nestedAnnotation, annotation.target().asClass(), testClass)) { + testResourceAnnotations.add(nestedAnnotation); + } + } } return testResourceAnnotations; } + private boolean keepTestResourceAnnotation(AnnotationInstance annotation, ClassInfo targetClass, Class testClass) { + AnnotationValue restrict = annotation.value("restrictToAnnotatedTest"); + if (restrict != null && restrict.asBoolean()) { + return testClass.getName().equals(targetClass.name().toString('.')); + } + return true; + } + public static class TestResourceClassEntry { private Class clazz; From 7674104233bfda217ae430304c90df3e28d35333 Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Tue, 9 Feb 2021 11:42:44 +0100 Subject: [PATCH 06/14] Added `@WithKubernetesTestServer` meta-annotation --- .../client/AbsentConfigMapPropertiesPMT.java | 2 +- .../client/ConfigMapPropertiesTest.java | 2 +- .../client/KubernetesClientTest.java | 2 +- .../client/KubernetesNewClientTest.java | 2 +- .../client/KubernetesTestServerTest.java | 12 ++----- .../NamespacedConfigMapPropertiesTest.java | 2 +- .../client/SecretPropertiesTest.java | 2 +- .../client/KubernetesServerTestResource.java | 35 ++++--------------- .../client/WithKubernetesTestServer.java | 35 +++++++++++++++++++ 9 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/WithKubernetesTestServer.java diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/AbsentConfigMapPropertiesPMT.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/AbsentConfigMapPropertiesPMT.java index b218f88090e9d..051b6271f2091 100644 --- a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/AbsentConfigMapPropertiesPMT.java +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/AbsentConfigMapPropertiesPMT.java @@ -11,7 +11,7 @@ import io.quarkus.test.QuarkusProdModeTest; import io.quarkus.test.common.QuarkusTestResource; -@QuarkusTestResource(CustomKubernetesMockServerTestResource.class) +@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedTest = true) public class AbsentConfigMapPropertiesPMT { @RegisterExtension diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesTest.java index f2f9e599aab57..dab151f6c39dc 100644 --- a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesTest.java +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/ConfigMapPropertiesTest.java @@ -8,7 +8,7 @@ import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; -@QuarkusTestResource(CustomKubernetesMockServerTestResource.class) +@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedTest = true) @QuarkusTest public class ConfigMapPropertiesTest { diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientTest.java index 69f9a35c237dc..6a914246d6e2a 100644 --- a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientTest.java +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientTest.java @@ -19,7 +19,7 @@ * KubernetesClientTest.TestResource contains the entire process of setting up the Mock Kubernetes API Server * It has to live there otherwise the Kubernetes client in native mode won't be able to locate the mock API Server */ -@QuarkusTestResource(CustomKubernetesMockServerTestResource.class) +@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedTest = true) @QuarkusTest public class KubernetesClientTest { diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesNewClientTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesNewClientTest.java index 7a82130dd121f..dcb11707b963a 100644 --- a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesNewClientTest.java +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesNewClientTest.java @@ -17,7 +17,7 @@ * KubernetesClientTest.TestResource contains the entire process of setting up the Mock Kubernetes API Server * It has to live there otherwise the Kubernetes client in native mode won't be able to locate the mock API Server */ -@QuarkusTestResource(CustomKubernetesTestServerTestResource.class) +@QuarkusTestResource(value = CustomKubernetesTestServerTestResource.class, restrictToAnnotatedTest = true) @QuarkusTest public class KubernetesNewClientTest { diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java index 16c043c9177c3..747190dbecb8c 100644 --- a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java @@ -7,20 +7,14 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.client.server.mock.KubernetesServer; -import io.quarkus.test.common.QuarkusTestResource; -import io.quarkus.test.common.ResourceArg; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTestProfile; import io.quarkus.test.junit.TestProfile; -import io.quarkus.test.kubernetes.client.KubernetesServerTestResource; import io.quarkus.test.kubernetes.client.KubernetesTestServer; +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer; @TestProfile(KubernetesTestServerTest.MyProfile.class) -@QuarkusTestResource(value = KubernetesServerTestResource.class, initArgs = { - @ResourceArg(name = KubernetesServerTestResource.HTTPS, value = "false"), - @ResourceArg(name = KubernetesServerTestResource.CRUD, value = "false"), - @ResourceArg(name = KubernetesServerTestResource.PORT, value = "10001"), -}) +@WithKubernetesTestServer(https = false, crud = true, port = 10001) @QuarkusTest public class KubernetesTestServerTest { @@ -30,7 +24,7 @@ public class KubernetesTestServerTest { @Test public void testConfiguration() throws InterruptedException { // we can't really test CRUD, and HTTPS doesn't work - Assertions.assertEquals(mockServer.getMockServer().getPort(), 10001); + Assertions.assertEquals(10001, mockServer.getMockServer().getPort()); } public static class MyProfile implements QuarkusTestProfile { diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/NamespacedConfigMapPropertiesTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/NamespacedConfigMapPropertiesTest.java index c73a419dea3bc..b56143e148925 100644 --- a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/NamespacedConfigMapPropertiesTest.java +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/NamespacedConfigMapPropertiesTest.java @@ -10,7 +10,7 @@ import io.quarkus.test.junit.QuarkusTestProfile; import io.quarkus.test.junit.TestProfile; -@QuarkusTestResource(CustomKubernetesMockServerTestResource.class) +@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedTest = true) @TestProfile(NamespacedConfigMapPropertiesTest.MyProfile.class) @QuarkusTest public class NamespacedConfigMapPropertiesTest { diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesTest.java index 05072d9326dab..06205e2f740c1 100644 --- a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesTest.java +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/SecretPropertiesTest.java @@ -8,7 +8,7 @@ import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; -@QuarkusTestResource(CustomKubernetesMockServerTestResource.class) +@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedTest = true) @QuarkusTest public class SecretPropertiesTest { diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java index be62d76be9826..97b75ab77a4bd 100644 --- a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java @@ -3,44 +3,23 @@ import java.lang.annotation.Annotation; import java.net.InetAddress; import java.util.Collections; -import java.util.Map; import io.fabric8.kubernetes.client.GenericKubernetesClient; import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.quarkus.test.common.QuarkusTestResourceConfigurableLifecycleManager; -public class KubernetesServerTestResource extends AbstractKubernetesTestResource { - - /** - * Configure HTTPS usage, defaults to false - */ - public final static String HTTPS = "https"; - /** - * Configure CRUD usage, defaults to true - */ - public final static String CRUD = "crud"; - /** - * Configure the port to use, defaults to 0, for the first available port - */ - public final static String PORT = "port"; +public class KubernetesServerTestResource extends AbstractKubernetesTestResource + implements QuarkusTestResourceConfigurableLifecycleManager { private boolean https = false; private boolean crud = true; private int port = 0; @Override - public void init(Map initArgs) { - String val = initArgs.get(HTTPS); - if (val != null) { - this.https = Boolean.parseBoolean(val); - } - val = initArgs.get(CRUD); - if (val != null) { - this.crud = Boolean.parseBoolean(val); - } - val = initArgs.get(PORT); - if (val != null) { - this.port = Integer.parseInt(val); - } + public void init(WithKubernetesTestServer annotation) { + this.https = annotation.https(); + this.crud = annotation.crud(); + this.port = annotation.port(); } @Override diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/WithKubernetesTestServer.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/WithKubernetesTestServer.java new file mode 100644 index 0000000000000..9517b67081354 --- /dev/null +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/WithKubernetesTestServer.java @@ -0,0 +1,35 @@ +package io.quarkus.test.kubernetes.client; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.quarkus.test.common.QuarkusTestResource; + +/** + * Use on your test resource to get a mock {@link KubernetesServer} spawn up, and injectable with {@link KubernetesTestServer}. + * This annotation is only active when used on a test class, and only for this test class. + */ +@QuarkusTestResource(value = KubernetesServerTestResource.class, restrictToAnnotatedTest = true) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface WithKubernetesTestServer { + + /** + * Start it with HTTPS + */ + boolean https() default false; + + /** + * Start it in CRUD mode + */ + boolean crud() default true; + + /** + * Port to use, defaults to any available port + */ + int port() default 0; + +} From f0ce346cfc8ad9b99bd084f22859aa3141d6fa1f Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Tue, 9 Feb 2021 14:53:40 +0100 Subject: [PATCH 07/14] Docs for new test resource meta-annotations --- .../asciidoc/getting-started-testing.adoc | 57 +++++++++++++++++++ docs/src/main/asciidoc/kubernetes-client.adoc | 17 +++--- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 8f69be4a2c16e..e57d59f3e224e 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -987,11 +987,68 @@ A very common need is to start some services on which your Quarkus application d By simply annotating any test in the test suite with `@QuarkusTestResource`, Quarkus will run the corresponding `QuarkusTestResourceLifecycleManager` before any tests are run. A test suite is also free to utilize multiple `@QuarkusTestResource` annotations, in which case all the corresponding `QuarkusTestResourceLifecycleManager` objects will be run before the tests. When using multiple test resources they can be started concurrently. For that you need to set `@QuarkusTestResource(parallel = true)`. +NOTE: test resources are global, even if they are defined on a test class, which means they will all be activated for all tests, even though we do +remove duplicates. If you want to only enable a test resource on a single test class, you can use `@QuarkusTestResource(restrictToAnnotatedTest = true)`. + Quarkus provides a few implementations of `QuarkusTestResourceLifecycleManager` out of the box (see `io.quarkus.test.h2.H2DatabaseTestResource` which starts an H2 database, or `io.quarkus.test.kubernetes.client.KubernetesMockServerTestResource` which starts a mock Kubernetes API server), but it is common to create custom implementations to address specific application needs. Common cases include starting docker containers using https://www.testcontainers.org/[Testcontainers] (an example of which can be found https://github.com/quarkusio/quarkus-quickstarts/blob/master/kafka-quickstart/src/test/java/org/acme/kafka/KafkaResource.java[here]), or starting a mock HTTP server using http://wiremock.org/[Wiremock] (an example of which can be found https://github.com/geoand/quarkus-test-demo/blob/master/src/test/java/org/acme/getting/started/country/WiremockCountries.java[here]). +=== Annotation-based test resources + +It is possible to write test resources that are enabled and configured using annotations. This is enabled by placing the `@QuarkusTestResource` +on an annotation which will be used to enable and configure the test resource. + +For example, this defines the `@WithKubernetesTestServer` annotation, which you can use on your tests to activate the `KubernetesServerTestResource`, +but only for the annotated test class. + +[source,java] +---- +@QuarkusTestResource(KubernetesServerTestResource.class) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface WithKubernetesTestServer { + /** + * Start it with HTTPS + */ + boolean https() default false; + + /** + * Start it in CRUD mode + */ + boolean crud() default true; + + /** + * Port to use, defaults to any available port + */ + int port() default 0; +} +---- + +The `KubernetesServerTestResource` class has to implement the +`QuarkusTestResourceConfigurableLifecycleManager` interface in order to be configured using the previous annotation: + +[source,java] +---- +public class KubernetesServerTestResource + implements QuarkusTestResourceConfigurableLifecycleManager { + + private boolean https = false; + private boolean crud = true; + private int port = 0; + + @Override + public void init(WithKubernetesTestServer annotation) { + this.https = annotation.https(); + this.crud = annotation.crud(); + this.port = annotation.port(); + } + + // ... +} +---- + == Hang Detection `@QuarkusTest` has support for hang detection to help diagnose any unexpected hangs. If no progress is made for a specified diff --git a/docs/src/main/asciidoc/kubernetes-client.adoc b/docs/src/main/asciidoc/kubernetes-client.adoc index 547a64ad48ac7..8624958617491 100644 --- a/docs/src/main/asciidoc/kubernetes-client.adoc +++ b/docs/src/main/asciidoc/kubernetes-client.adoc @@ -71,9 +71,9 @@ public class KubernetesClientProducer { == Testing -To make testing against a mock Kubernetes API extremely simple, Quarkus provides the `KubernetesMockServerTestResource` which automatically launches +To make testing against a mock Kubernetes API extremely simple, Quarkus provides the `WithKubernetesTestServer` annotation which automatically launches a mock of the Kubernetes API server and sets the proper environment variables needed so that the Kubernetes Client configures itself to use said mock. -Tests can inject the mock and set it up in any way necessary for the particular testing using the `@MockServer` annotation. +Tests can inject the mock server and set it up in any way necessary for the particular testing using the `@KubernetesTestServer` annotation. Let's assume we have a REST endpoint defined like so: @@ -100,12 +100,13 @@ We could write a test for this endpoint very easily like so: [source%nowrap,java] ---- -@QuarkusTestResource(KubernetesMockServerTestResource.class) +// you can even configure aspects like crud, https and port on this annotation +@WithKubernetesTestServer @QuarkusTest public class KubernetesClientTest { - @MockServer - KubernetesMockServer mockServer; + @KubernetesTestServer + KubernetesServer mockServer; @BeforeEach public void before() { @@ -139,14 +140,14 @@ Note that to take advantage of these features, the `quarkus-test-kubernetes-clie ---- -You can create a `CustomKubernetesMockServerTestResource.java` to ensure all your `@QuarkusTest` enabled test classes share the same mock server setup: +Alternately, you can create a `CustomKubernetesMockServerTestResource.java` to ensure all your `@QuarkusTest` enabled test classes share the same mock server setup: [source%nowrap,java] ---- -public class CustomKubernetesMockServerTestResource extends KubernetesMockServerTestResource { +public class CustomKubernetesMockServerTestResource extends KubernetesServerTestResource { @Override - public void configureMockServer(KubernetesMockServer mockServer) { + public void configureMockServer(KubernetesServer mockServer) { mockServer.expect().get().withPath("/api/v1/namespaces/test/pods") .andReturn(200, new PodList()) .always(); From 18c109199306d2a9a3694c22ccf9443934392284 Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Wed, 10 Feb 2021 16:18:23 +0100 Subject: [PATCH 08/14] Kubernetes test server: added setup class option --- .../client/KubernetesTestServerTest.java | 15 ++++++++++++++- .../client/KubernetesServerTestResource.java | 13 +++++++++++++ .../client/WithKubernetesTestServer.java | 11 +++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java index 747190dbecb8c..23d5ea0e8c693 100644 --- a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesTestServerTest.java @@ -2,6 +2,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -14,10 +15,21 @@ import io.quarkus.test.kubernetes.client.WithKubernetesTestServer; @TestProfile(KubernetesTestServerTest.MyProfile.class) -@WithKubernetesTestServer(https = false, crud = true, port = 10001) +@WithKubernetesTestServer(https = false, crud = true, port = 10001, setup = KubernetesTestServerTest.Setup.class) @QuarkusTest public class KubernetesTestServerTest { + private static KubernetesServer setupServer; + + public static class Setup implements Consumer { + + @Override + public void accept(KubernetesServer t) { + setupServer = t; + } + + } + @KubernetesTestServer private KubernetesServer mockServer; @@ -25,6 +37,7 @@ public class KubernetesTestServerTest { public void testConfiguration() throws InterruptedException { // we can't really test CRUD, and HTTPS doesn't work Assertions.assertEquals(10001, mockServer.getMockServer().getPort()); + Assertions.assertSame(mockServer, setupServer); } public static class MyProfile implements QuarkusTestProfile { diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java index 97b75ab77a4bd..99b0d893e6800 100644 --- a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/KubernetesServerTestResource.java @@ -3,6 +3,7 @@ import java.lang.annotation.Annotation; import java.net.InetAddress; import java.util.Collections; +import java.util.function.Consumer; import io.fabric8.kubernetes.client.GenericKubernetesClient; import io.fabric8.kubernetes.client.server.mock.KubernetesServer; @@ -14,12 +15,18 @@ public class KubernetesServerTestResource extends AbstractKubernetesTestResource private boolean https = false; private boolean crud = true; private int port = 0; + private Consumer setup; @Override public void init(WithKubernetesTestServer annotation) { this.https = annotation.https(); this.crud = annotation.crud(); this.port = annotation.port(); + try { + this.setup = annotation.setup().newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } } @Override @@ -32,6 +39,12 @@ protected void initServer() { server.before(); } + @Override + protected void configureServer() { + if (setup != null) + setup.accept(server); + } + @Override protected KubernetesServer createServer() { return new KubernetesServer(https, crud, InetAddress.getLoopbackAddress(), port, Collections.emptyList()); diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/WithKubernetesTestServer.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/WithKubernetesTestServer.java index 9517b67081354..5dfbb472ec61d 100644 --- a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/WithKubernetesTestServer.java +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/WithKubernetesTestServer.java @@ -4,6 +4,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.function.Consumer; import io.fabric8.kubernetes.client.server.mock.KubernetesServer; import io.quarkus.test.common.QuarkusTestResource; @@ -32,4 +33,14 @@ */ int port() default 0; + /** + * Setup class to call after the mock server is created, for custom setup. + */ + Class> setup() default NO_SETUP.class; + + static class NO_SETUP implements Consumer { + @Override + public void accept(KubernetesServer t) { + } + }; } From f8bf4655894a949c562d1d810298c7f4ce350c02 Mon Sep 17 00:00:00 2001 From: Stephane Epardaud Date: Mon, 15 Feb 2021 10:46:13 +0100 Subject: [PATCH 09/14] Fix test order for k8s client In order to make sure tests fail if we get per-test resources wrong --- integration-tests/kubernetes-client/pom.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integration-tests/kubernetes-client/pom.xml b/integration-tests/kubernetes-client/pom.xml index dc37c18c7b122..ea2df35547387 100644 --- a/integration-tests/kubernetes-client/pom.xml +++ b/integration-tests/kubernetes-client/pom.xml @@ -93,6 +93,9 @@ org.apache.maven.plugins maven-surefire-plugin + + alphabetical +