From f4ae2631609ed5710776a000565c5446b4dfc5ee Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Tue, 18 Jun 2024 10:29:25 +0200 Subject: [PATCH] test: added tests for KubernetesClient serialization/deserialization Should ensure that (de)serialization works as expected both in JVM and Native modes. Signed-off-by: Marc Nuri --- .../io/quarkus/it/kubernetes/client/Pods.java | 36 ++++- .../quarkus/it/kubernetes/client/Version.java | 2 + .../KubernetesClientSerializationIT.java | 7 + .../KubernetesClientSerializationTest.java | 144 ++++++++++++++++++ 4 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientSerializationIT.java create mode 100644 integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientSerializationTest.java 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 de54905a37ca6..4930097ae72bb 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 @@ -3,23 +3,29 @@ import java.util.List; import java.util.UUID; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.NonDeletingOperation; @Path("/pod") public class Pods { private final KubernetesClient kubernetesClient; + @Inject public Pods(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; } @@ -38,7 +44,7 @@ public Response deleteFirst(@PathParam("namespace") String namespace) { return Response.status(404).build(); } - kubernetesClient.pods().inNamespace(namespace).delete(pods.get(0)); + kubernetesClient.pods().inNamespace(namespace).resource(pods.get(0)).delete(); return Response.noContent().build(); } @@ -53,12 +59,13 @@ 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) + Pod updatedPod = new PodBuilder().withNewMetadata() + .withName(podName) .addToAnnotations("test-reference", "12345") .addToLabels("key1", "value1").endMetadata() .build(); - updatedPod = kubernetesClient.pods().withName(podName).createOrReplace(updatedPod); + updatedPod = kubernetesClient.pods().resource(updatedPod).createOr(NonDeletingOperation::update); return Response.ok(updatedPod).build(); } @@ -67,10 +74,29 @@ public Response updateFirst(@PathParam("namespace") String namespace) { public Response createNew(@PathParam("namespace") String namespace) { return Response .ok(kubernetesClient.pods().inNamespace(namespace) - .create(new PodBuilder().withNewMetadata() + .resource(new PodBuilder().withNewMetadata() .withName(UUID.randomUUID().toString()) .addToAnnotations("test-reference", "12345") - .endMetadata().build())) + .endMetadata().build()) + .create()) .build(); } + + @GET + @Path("/{namespace}/{podName}") + @Produces(MediaType.APPLICATION_JSON) + public Response get(@PathParam("namespace") String namespace, @PathParam("podName") String name) { + return Response.ok(kubernetesClient.pods().inNamespace(namespace).withName(name).get()).build(); + } + + @PUT + @Path("/{namespace}/{podName}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response upsert(@PathParam("namespace") String namespace, @PathParam("podName") String name, Pod pod) { + final Pod updatedPod = kubernetesClient.pods().inNamespace(namespace) + .resource(new PodBuilder(pod).editMetadata().withName(name).endMetadata().build()) + .createOr(NonDeletingOperation::update); + return Response.ok(updatedPod).build(); + } } diff --git a/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/Version.java b/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/Version.java index 0bce4bba85ff9..ed874fd4d700f 100644 --- a/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/Version.java +++ b/integration-tests/kubernetes-client/src/main/java/io/quarkus/it/kubernetes/client/Version.java @@ -1,5 +1,6 @@ package io.quarkus.it.kubernetes.client; +import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Response; @@ -11,6 +12,7 @@ public class Version { private final KubernetesClient kubernetesClient; + @Inject public Version(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; } diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientSerializationIT.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientSerializationIT.java new file mode 100644 index 0000000000000..f2d3f4878248c --- /dev/null +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientSerializationIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.kubernetes.client; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class KubernetesClientSerializationIT extends KubernetesClientSerializationTest { +} diff --git a/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientSerializationTest.java b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientSerializationTest.java new file mode 100644 index 0000000000000..96a1c9444f732 --- /dev/null +++ b/integration-tests/kubernetes-client/src/test/java/io/quarkus/it/kubernetes/client/KubernetesClientSerializationTest.java @@ -0,0 +1,144 @@ +package io.quarkus.it.kubernetes.client; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import java.util.function.Consumer; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.ContainerPortBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.PodSpecBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.api.model.VolumeMount; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.kubernetes.client.KubernetesTestServer; +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer; +import io.restassured.filter.log.LogDetail; + +@WithKubernetesTestServer(setup = KubernetesClientSerializationTest.CrudEnvironmentPreparation.class) +@QuarkusTest +public class KubernetesClientSerializationTest { + + @KubernetesTestServer + private KubernetesServer mockServer; + + @Test + @DisplayName("PUT with String body - Should serialize and persist the Pod") + void serialization() { + given() + .contentType("application/json") + .body(""" + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "serialization-test" + }, + "spec": { + "containers": [ + { + "name": "test", + "image": "busybox", + "volumeMounts": [ + { + "name": "test", + "mountPath": "/test" + } + ] + } + ] + } + } + """) + .when() + .put("/pod/{namespace}/{podName}", mockServer.getClient().getNamespace(), "serialization-test") + .then() + .statusCode(200); + // JSON data is persisted into the mock server + assertThat(mockServer.getClient().pods().withName("serialization-test").get()) + .hasFieldOrPropertyWithValue("metadata.name", "serialization-test") + .extracting("spec.containers").asInstanceOf(InstanceOfAssertFactories.list(Container.class)) + .singleElement() + .hasFieldOrPropertyWithValue("name", "test") + .hasFieldOrPropertyWithValue("image", "busybox") + .extracting(Container::getVolumeMounts) + .asInstanceOf(InstanceOfAssertFactories.list(VolumeMount.class)) + .singleElement() + .hasFieldOrPropertyWithValue("name", "test") + .hasFieldOrPropertyWithValue("mountPath", "/test"); + } + + @Test + @DisplayName("GET - Should deserialize the Pod into valid JSON") + void deserialization() { + mockServer.getClient().pods().resource(new PodBuilder() + .withMetadata(new ObjectMetaBuilder() + .withName("deserialization-test") + .build()) + .withSpec(new PodSpecBuilder() + .addToContainers(new ContainerBuilder() + .withName("deserialization-test-container") + .addToPorts(new ContainerPortBuilder() + .withContainerPort(8080) + .build()) + .build()) + .build()) + .build()) + .create(); + // JSON data is retrieved from the mock server + when() + .get("/pod/{namespace}/{podName}", mockServer.getClient().getNamespace(), "deserialization-test") + .then() + .statusCode(200) + .body( + "metadata.name", is("deserialization-test"), + "spec.containers[0].name", is("deserialization-test-container"), + "spec.containers[0].ports[0].containerPort", is(8080), + "metadata.annotations", nullValue(), + // https://github.com/quarkusio/quarkus/issues/39934 + "spec.overhead", nullValue()) + .log().ifValidationFails(LogDetail.BODY); + } + + public static final class CrudEnvironmentPreparation implements Consumer { + + @Override + public void accept(KubernetesServer kubernetesServer) { + final KubernetesClient kc = kubernetesServer.getClient(); + kc.configMaps().resource(new ConfigMapBuilder() + .withNewMetadata().withName("cmap1").endMetadata() + .addToData("dummy", "I'm required") + .build()).create(); + kc.configMaps().resource(new ConfigMapBuilder() + .withNewMetadata().withName("cmap2").endMetadata() + .addToData("dummysecret", "dumb") + .addToData("overridden.secret", "Alex") + .addToData("some.prop1", "I'm required") + .addToData("some.prop2", "I'm required (2)") + .addToData("some.prop3", "I'm required (3)") + .addToData("some.prop4", "I'm required (4)") + .addToData("some.prop5", "I'm required (5)") + .build()).create(); + kc.secrets().resource(new SecretBuilder() + .withNewMetadata().withName("s1").endMetadata() + .addToData("secret.prop1", "c2VjcmV0") + .addToData("secret.prop2", "c2VjcmV0") + .addToData("secret.prop3", "c2VjcmV0") + .addToData("secret.prop4", "c2VjcmV0") + .build()).create(); + } + } +}