From 965caac41b05cbeb8ee1070b16424f2798791993 Mon Sep 17 00:00:00 2001 From: Steve Hawkins Date: Wed, 26 Oct 2022 08:35:01 -0400 Subject: [PATCH 1/3] fix #4530: allowing serialization to better handle primitive values --- CHANGELOG.md | 1 + .../client/utils/Serialization.java | 21 ++++++++++-- .../client/utils/SerializationTest.java | 32 ++++++++++++++++--- .../api/model/runtime/RawExtension.java | 2 ++ .../internal/KubernetesDeserializer.java | 7 +++- 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4918b93b3bb..51060a191dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ #### Improvements * Fix #4355: for exec, attach, upload, and copy operations the container id/name will be validated or chosen prior to the remote call. You may also use the kubectl.kubernetes.io/default-container annotation to specify the default container. +* Fix #4530: generalizing the Serialization logic to allow for primitive values and clarifying the type expectations. #### Dependency Upgrade diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Serialization.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Serialization.java index 2bd954316fb..a5ffeb936b5 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Serialization.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Serialization.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import io.fabric8.kubernetes.api.model.KubernetesResource; +import io.fabric8.kubernetes.api.model.runtime.RawExtension; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.model.jackson.UnmatchedFieldTypeModule; import org.yaml.snakeyaml.DumperOptions; @@ -158,6 +159,8 @@ public static String asYaml(T object) { /** * Unmarshals a stream. + *

+ * The type is assumed to be {@link KubernetesResource} * * @param is The {@link InputStream}. * @param The target type. @@ -170,6 +173,8 @@ public static T unmarshal(InputStream is) { /** * Unmarshals a stream optionally performing placeholder substitution to the stream. + *

+ * The type is assumed to be {@link KubernetesResource} * * @param is The {@link InputStream}. * @param parameters A {@link Map} with parameters for placeholder substitution. @@ -190,6 +195,8 @@ public static T unmarshal(InputStream is, Map parameters) { /** * Unmarshals a stream. + *

+ * The type is assumed to be {@link KubernetesResource} * * @param is The {@link InputStream}. * @param mapper The {@link ObjectMapper} to use. @@ -202,6 +209,8 @@ public static T unmarshal(InputStream is, ObjectMapper mapper) { /** * Unmarshals a stream optionally performing placeholder substitution to the stream. + *

+ * The type is assumed to be {@link KubernetesResource} * * @param is The {@link InputStream}. * @param mapper The {@link ObjectMapper} to use. @@ -231,11 +240,15 @@ private static T unmarshal(InputStream is, ObjectMapper mapper, TypeReferenc bis.reset(); final T result; - if (intch != '{') { + if (intch != '{' && intch != '[') { final Yaml yaml = new Yaml(new SafeConstructor(), new Representer(), new DumperOptions(), new CustomYamlTagResolverWithLimit()); - final Map obj = yaml.load(bis); - result = mapper.convertValue(obj, type); + final Object obj = yaml.load(bis); + if (obj instanceof Map) { + result = mapper.convertValue(obj, type); + } else { + result = mapper.convertValue(new RawExtension(obj), type); + } } else { result = mapper.readerFor(type).readValue(bis); } @@ -247,6 +260,8 @@ private static T unmarshal(InputStream is, ObjectMapper mapper, TypeReferenc /** * Unmarshals a {@link String} + *

+ * The type is assumed to be {@link KubernetesResource} * * @param str The {@link String}. * @param template argument denoting type diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationTest.java index dac7aba48a4..49e68a1f8f2 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationTest.java @@ -45,6 +45,7 @@ import io.fabric8.kubernetes.api.model.coordination.v1.LeaseSpec; import io.fabric8.kubernetes.api.model.runtime.RawExtension; import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.Version; import org.assertj.core.api.InstanceOfAssertFactories; @@ -61,6 +62,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -235,15 +237,37 @@ void unmarshalWithValidCustomResourceShouldReturnGenericCustomResource() { } @Test - @DisplayName("unmarshal, with invalid YAML content, should throw Exception") void unmarshalWithInvalidYamlShouldThrowException() { // Given final InputStream is = SerializationTest.class.getResourceAsStream("/serialization/invalid-yaml.yml"); // When - final ClassCastException result = assertThrows(ClassCastException.class, () -> Serialization.unmarshal(is)); + RawExtension result = Serialization.unmarshal(is); // Then - assertThat(result) - .hasMessageContainingAll("java.lang.String", "cannot be cast to", "java.util.Map"); + assertEquals("This\nis not\nYAML", result.getValue()); + } + + @Test + void unmarshalArrays() { + // not valid as KubernetesResource - we'd have to look ahead to know if the array values + // were not hasmetadata + assertThrows(KubernetesClientException.class, () -> Serialization.unmarshal("[1, 2]")); + assertThrows(IllegalArgumentException.class, () -> Serialization.unmarshal("- 1\n- 2")); + + assertEquals(Arrays.asList(1, 2), Serialization.unmarshal("[1, 2]", List.class)); + assertEquals(Arrays.asList(1, 2), Serialization.unmarshal("- 1\n- 2", List.class)); + } + + @Test + void unmarshalPrimitives() { + // as json + RawExtension raw = Serialization.unmarshal("\"a\""); + assertEquals("a", raw.getValue()); + // as yaml + raw = Serialization.unmarshal("a"); + assertEquals("a", raw.getValue()); + + assertEquals("a", Serialization.unmarshal("\"a\"", String.class)); + assertEquals("a", Serialization.unmarshal("a", String.class)); } @Test diff --git a/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/runtime/RawExtension.java b/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/runtime/RawExtension.java index 2f1aa79ce61..cd86f06bcd5 100644 --- a/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/runtime/RawExtension.java +++ b/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/api/model/runtime/RawExtension.java @@ -21,7 +21,9 @@ import io.fabric8.kubernetes.api.model.AnyType; import io.fabric8.kubernetes.api.model.KubernetesResource; import io.sundr.builder.annotations.Buildable; +import lombok.ToString; +@ToString(callSuper = true) @JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None.class) @Buildable(editableEnabled = false, validationEnabled = false, generateBuilderPackage = true, lazyCollectionInitEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder") public class RawExtension extends AnyType implements KubernetesResource { diff --git a/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/internal/KubernetesDeserializer.java b/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/internal/KubernetesDeserializer.java index f2c29f77780..3417e3296e7 100644 --- a/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/internal/KubernetesDeserializer.java +++ b/kubernetes-model-generator/kubernetes-model-core/src/main/java/io/fabric8/kubernetes/internal/KubernetesDeserializer.java @@ -89,9 +89,12 @@ public KubernetesResource deserialize(JsonParser jp, DeserializationContext ctxt return fromObjectNode(jp, node); } else if (node.isArray()) { return fromArrayNode(jp, node); - } else { + } + Object object = node.traverse(jp.getCodec()).readValueAs(Object.class); + if (object == null) { return null; } + return new RawExtension(object); } private KubernetesResource fromArrayNode(JsonParser jp, JsonNode node) throws IOException { @@ -105,6 +108,8 @@ private KubernetesResource fromArrayNode(JsonParser jp, JsonNode node) throws IO throw new JsonMappingException(jp, "Cannot parse a nested array containing a non-HasMetadata resource"); } list.add((HasMetadata) resource); + } else { + throw new JsonMappingException(jp, "Cannot parse a nested array containing non-object resource"); } } return new KubernetesListBuilder().withItems(list).build(); From ce7dd651ba3bd6e192cb3f58b688dde246cf6d0f Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Fri, 11 Nov 2022 09:54:56 +0100 Subject: [PATCH 2/3] refactor: add parameterized tests for Serialization utils Signed-off-by: Marc Nuri --- ...ializationSingleDocumentUnmarshalTest.java | 32 +++++---- .../client/utils/SerializationTest.java | 69 ++++++++++++++----- 2 files changed, 71 insertions(+), 30 deletions(-) diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationSingleDocumentUnmarshalTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationSingleDocumentUnmarshalTest.java index 6b039fb5e2a..d8bf77536ef 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationSingleDocumentUnmarshalTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationSingleDocumentUnmarshalTest.java @@ -27,24 +27,28 @@ class SerializationSingleDocumentUnmarshalTest { @ParameterizedTest(name = "#{index} - unmarshal {0}") - @ValueSource(strings = {"document-with-trailing-document-delimiter.yml", "document-with-leading-document-delimiter.yml", "document-with-leading-and-trailing-document-delimiter.yml", "document-with-no-document-delimiter.yml"}) + @ValueSource(strings = { + "document-with-trailing-document-delimiter.yml", + "document-with-leading-document-delimiter.yml", + "document-with-leading-and-trailing-document-delimiter.yml", + "document-with-no-document-delimiter.yml" + }) void unmarshalWithSingleDocumentWithDocumentDelimiterShouldReturnKubernetesResource(String arg) { // When final KubernetesResource result = Serialization.unmarshal( - SerializationTest.class.getResourceAsStream(String.format("/serialization/%s", arg)), - Collections.emptyMap() - ); + SerializationTest.class.getResourceAsStream(String.format("/serialization/%s", arg)), + Collections.emptyMap()); // Then assertThat(result) - .asInstanceOf(InstanceOfAssertFactories.type(Service.class)) - .hasFieldOrPropertyWithValue("apiVersion", "v1") - .hasFieldOrPropertyWithValue("kind", "Service") - .hasFieldOrPropertyWithValue("metadata.name", "redis-master") - .hasFieldOrPropertyWithValue("metadata.labels.app", "redis") - .hasFieldOrPropertyWithValue("metadata.labels.tier", "backend") - .hasFieldOrPropertyWithValue("metadata.labels.role", "master") - .hasFieldOrPropertyWithValue("spec.selector.app", "redis") - .hasFieldOrPropertyWithValue("spec.selector.tier", "backend") - .hasFieldOrPropertyWithValue("spec.selector.role", "master"); + .asInstanceOf(InstanceOfAssertFactories.type(Service.class)) + .hasFieldOrPropertyWithValue("apiVersion", "v1") + .hasFieldOrPropertyWithValue("kind", "Service") + .hasFieldOrPropertyWithValue("metadata.name", "redis-master") + .hasFieldOrPropertyWithValue("metadata.labels.app", "redis") + .hasFieldOrPropertyWithValue("metadata.labels.tier", "backend") + .hasFieldOrPropertyWithValue("metadata.labels.role", "master") + .hasFieldOrPropertyWithValue("spec.selector.app", "redis") + .hasFieldOrPropertyWithValue("spec.selector.tier", "backend") + .hasFieldOrPropertyWithValue("spec.selector.role", "master"); } } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationTest.java index 49e68a1f8f2..e604755000e 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/SerializationTest.java @@ -52,6 +52,9 @@ import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.io.File; import java.io.IOException; @@ -65,10 +68,12 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; class SerializationTest { @@ -195,7 +200,7 @@ void testSerializeYamlWithAlias() { } @Test - void testClone() { + void cloneKubernetesResourceReturnsDifferentInstance() { // Given Pod pod = new PodBuilder().withNewMetadata().withName("pod").endMetadata().build(); // When @@ -208,7 +213,7 @@ void testClone() { } @Test - void testCloneNonResource() { + void cloneNonResourceReturnsDifferentInstance() { // Given Map value = Collections.singletonMap("key", "value"); // When @@ -237,7 +242,7 @@ void unmarshalWithValidCustomResourceShouldReturnGenericCustomResource() { } @Test - void unmarshalWithInvalidYamlShouldThrowException() { + void unmarshalWithInvalidYamlShouldReturnRawExtension() { // Given final InputStream is = SerializationTest.class.getResourceAsStream("/serialization/invalid-yaml.yml"); // When @@ -247,29 +252,61 @@ void unmarshalWithInvalidYamlShouldThrowException() { } @Test - void unmarshalArrays() { + void unmarshalYamlArrayShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> Serialization.unmarshal("- 1\n- 2")) + .withMessageStartingWith("Cannot parse a nested array containing non-object resource"); + } + + @Test + void unmarshalJsonArrayShouldThrowException() { + assertThatExceptionOfType(KubernetesClientException.class) + .isThrownBy(() -> Serialization.unmarshal("[1, 2]")) + .withMessage("An error has occurred.") + .havingCause() + .withMessageStartingWith("Cannot parse a nested array containing non-object resource"); + } + + @Test + void unmarshalYamlArrayWithProvidedTypeShouldDeserialize() { // not valid as KubernetesResource - we'd have to look ahead to know if the array values // were not hasmetadata - assertThrows(KubernetesClientException.class, () -> Serialization.unmarshal("[1, 2]")); - assertThrows(IllegalArgumentException.class, () -> Serialization.unmarshal("- 1\n- 2")); - - assertEquals(Arrays.asList(1, 2), Serialization.unmarshal("[1, 2]", List.class)); assertEquals(Arrays.asList(1, 2), Serialization.unmarshal("- 1\n- 2", List.class)); } @Test - void unmarshalPrimitives() { - // as json - RawExtension raw = Serialization.unmarshal("\"a\""); - assertEquals("a", raw.getValue()); - // as yaml - raw = Serialization.unmarshal("a"); - assertEquals("a", raw.getValue()); + void unmarshalJsonArrayWithProvidedTypeShouldDeserialize() { + // not valid as KubernetesResource - we'd have to look ahead to know if the array values + // were not hasmetadata + assertEquals(Arrays.asList(1, 2), Serialization.unmarshal("[1, 2]", List.class)); + } + @ParameterizedTest(name = "''{0}'' should be deserialized as ''{1}''") + @MethodSource("unmarshalPrimitivesInput") + void unmarshalPrimitives(String input, Object expected) { + assertThat(Serialization. unmarshal(input)) + .extracting(RawExtension::getValue) + .isEqualTo(expected); assertEquals("a", Serialization.unmarshal("\"a\"", String.class)); assertEquals("a", Serialization.unmarshal("a", String.class)); } + @ParameterizedTest(name = "''{0}'' and ''{2}'' target type should be deserialized as ''{1}''") + @MethodSource("unmarshalPrimitivesInput") + void unmarshalPrimitivesWithType(String input, Object expected, Class targetType) { + assertThat(Serialization.unmarshal(input, targetType)) + .isEqualTo(expected); + } + + static Stream unmarshalPrimitivesInput() { + return Stream.of( + Arguments.arguments("\"a\"", "a", String.class), // JSON + Arguments.arguments("a", "a", String.class), // YAML + Arguments.arguments("1", 1, Integer.class), + Arguments.arguments("true", true, Boolean.class), + Arguments.arguments("1.2", 1.2, Double.class)); + } + @Test void unmarshalRawResource() { InputStream is = SerializationTest.class.getResourceAsStream("/serialization/invalid-resource.yml"); From 6d37cba66bb94ef293c9f5bb6594c186fbb92988 Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Fri, 11 Nov 2022 10:51:53 +0100 Subject: [PATCH 3/3] feat: provided additional KubernetesClient.resource(InputStream) method Signed-off-by: Marc Nuri --- .../kubernetes/client/KubernetesClient.java | 21 ++++-- .../NamespacedKubernetesClientAdapter.java | 5 ++ .../client/impl/KubernetesClientImpl.java | 18 ++++- .../client/impl/KubernetesClientImplTest.java | 67 +++++++++++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java index 76e10ef3810..6d2f84ab984 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java @@ -286,10 +286,10 @@ MixedOperation> componentstatuses(); /** - * Load a Kubernetes resource object from file InputStream + * Load Kubernetes resource object(s) from the provided InputStream. * - * @param is File input stream object containing json/yaml content - * @return deserialized object + * @param is the input stream containing JSON/YAML content + * @return an operation instance to work on the list of Kubernetes Resource objects */ ParameterNamespaceListVisitFromServerGetDeleteRecreateWaitApplicable load(InputStream is); @@ -297,7 +297,7 @@ MixedOperation resourceList(String s); @@ -340,11 +340,20 @@ NamespaceListVisitFromServerGetDeleteRecreateWaitApplicable resourc * KubernetesResource operations. You can pass any Kubernetes resource as string object and do * all operations * - * @param s Kubernetes resource object as string + * @param s a Kubernetes resource object as string * @return operations object for Kubernetes resource */ NamespaceableResource resource(String s); + /** + * KubernetesResource operations. You can pass any Kubernetes resource as an InputStream object and perform + * all operations + * + * @param is the InputStream containing a serialized Kubernetes resource. + * @return operations object for Kubernetes resource. + */ + NamespaceableResource resource(InputStream is); + /** * Operations for Binding resource in APIgroup core/v1 * @@ -514,7 +523,7 @@ NamespaceListVisitFromServerGetDeleteRecreateWaitApplicable resourc /** * Visit all resources with the given {@link ApiVisitor}. - * + * * @param visitor */ void visitResources(ApiVisitor visitor); diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java index 25bb532bee0..7bd8982778a 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java @@ -283,6 +283,11 @@ public NamespaceableResource resource(String s) { return getClient().resource(s); } + @Override + public NamespaceableResource resource(InputStream is) { + return getClient().resource(is); + } + @Override public MixedOperation, Resource> bindings() { return getClient().bindings(); diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/KubernetesClientImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/KubernetesClientImpl.java index 35265ac6312..991eee4c53e 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/KubernetesClientImpl.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/KubernetesClientImpl.java @@ -357,12 +357,28 @@ public NamespaceableResource resource(T item) { return new NamespaceableResourceAdapter<>(item, op); } + private NamespaceableResource resource(Object resource) { + if (resource instanceof HasMetadata) { + return resource((HasMetadata) resource); + } + throw new KubernetesClientException("Unable to create a valid resource from the provided object (" + + resource.getClass().getName() + ")"); + } + /** * {@inheritDoc} */ @Override public NamespaceableResource resource(String s) { - return resource((HasMetadata) Serialization.unmarshal(s)); + return resource(Serialization. unmarshal(s)); + } + + /** + * {@inheritDoc} + */ + @Override + public NamespaceableResource resource(InputStream is) { + return resource(Serialization. unmarshal(is)); } /** diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/KubernetesClientImplTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/KubernetesClientImplTest.java index ca4b4203658..440ddc6eb33 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/KubernetesClientImplTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/KubernetesClientImplTest.java @@ -16,11 +16,14 @@ package io.fabric8.kubernetes.client.impl; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.ParameterNamespaceListVisitFromServerGetDeleteRecreateWaitApplicable; import io.fabric8.kubernetes.client.http.BasicBuilder; import io.fabric8.kubernetes.client.http.HttpHeaders; import io.fabric8.kubernetes.client.utils.HttpClientUtils; @@ -35,6 +38,7 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -44,6 +48,8 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -153,4 +159,65 @@ void shouldPropagateImpersonateSettings() { assertArrayEquals(new String[] { "b" }, currentConfig.getImpersonateGroups()); assertEquals(Collections.singletonList("d"), currentConfig.getImpersonateExtras().get("c")); } + + @Test + @DisplayName("resource(String).get with HasMetadata should deserialize") + void resourceFromStringWithHasMetadata() { + assertThat(new KubernetesClientImpl().resource("apiVersion: v1\nkind: Pod").get()) + .isInstanceOf(Pod.class); + } + + @Test + @DisplayName("resource(String) with no HasMetadata should throwException") + void resourceFromStringWithInvalid() { + final KubernetesClient kc = new KubernetesClientImpl(); + assertThatExceptionOfType(KubernetesClientException.class) + .isThrownBy(() -> kc.resource("NotAPod")) + .withMessageStartingWith("Unable to create a valid resource from the provided object"); + } + + @Test + @DisplayName("resource(InputStream).get with HasMetadata should deserialize") + void resourceFromInputStreamWithHasMetadata() throws IOException { + final String podYaml = "apiVersion: v1\nkind: Pod"; + try (InputStream is = new ByteArrayInputStream(podYaml.getBytes(StandardCharsets.UTF_8))) { + assertThat(new KubernetesClientImpl().resource(is).get()) + .isInstanceOf(Pod.class); + } + } + + @Test + @DisplayName("resource(InputStream) with no HasMetadata should throwException") + void resourceFromInputStreamWithInvalid() throws IOException { + final KubernetesClient kc = new KubernetesClientImpl(); + final String podYaml = "NotAPod"; + try (InputStream is = new ByteArrayInputStream(podYaml.getBytes(StandardCharsets.UTF_8))) { + assertThatExceptionOfType(KubernetesClientException.class) + .isThrownBy(() -> kc.resource(is)) + .withMessageStartingWith("Unable to create a valid resource from the provided object"); + } + } + + @Test + @DisplayName("load(InputStream).get with HasMetadata should deserialize") + void loadFromInputStreamWithHasMetadata() throws IOException { + final String podYaml = "apiVersion: v1\nkind: Pod"; + try (InputStream is = new ByteArrayInputStream(podYaml.getBytes(StandardCharsets.UTF_8))) { + assertThat(new KubernetesClientImpl().load(is).get()) + .containsExactly(new Pod()); + } + } + + @Test + @DisplayName("load(InputStream).get with no HasMetadata should throwException") + void loadFromInputStreamWithInvalid() throws IOException { + final String podYaml = "NotAPod"; + try (InputStream is = new ByteArrayInputStream(podYaml.getBytes(StandardCharsets.UTF_8))) { + final ParameterNamespaceListVisitFromServerGetDeleteRecreateWaitApplicable load = new KubernetesClientImpl() + .load(is); + assertThatIllegalArgumentException() + .isThrownBy(load::get) + .withMessageStartingWith("Could not convert item to a list of HasMetadata"); + } + } }