From 0b67950590ab99a5b06cbcbc69d61d506ecf6abb Mon Sep 17 00:00:00 2001 From: stan-sz <37585349+stan-sz@users.noreply.github.com> Date: Fri, 14 Oct 2022 11:44:47 +0200 Subject: [PATCH] Provide strict k8s yaml model deserializer (#1047) * Provide strict k8s yaml model deserializer * Provide documentation * Add UT coverage --- src/KubernetesClient.Models/KubernetesYaml.cs | 47 +++++++++++-------- .../KubernetesClientConfigurationTests.cs | 1 + .../KubernetesYamlTests.cs | 4 ++ 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/KubernetesClient.Models/KubernetesYaml.cs b/src/KubernetesClient.Models/KubernetesYaml.cs index dd79e9d7..c677bcbf 100644 --- a/src/KubernetesClient.Models/KubernetesYaml.cs +++ b/src/KubernetesClient.Models/KubernetesYaml.cs @@ -15,16 +15,22 @@ namespace k8s /// public static class KubernetesYaml { - private static readonly IDeserializer Deserializer = + private static readonly DeserializerBuilder CommonDeserializerBuilder = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeConverter(new IntOrStringYamlConverter()) .WithTypeConverter(new ByteArrayStringYamlConverter()) .WithTypeConverter(new ResourceQuantityYamlConverter()) .WithAttemptingUnquotedStringTypeDeserialization() - .WithOverridesFromJsonPropertyAttributes() - .IgnoreUnmatchedProperties() - .Build(); + .WithOverridesFromJsonPropertyAttributes(); + private static readonly IDeserializer StrictDeserializer = + CommonDeserializerBuilder + .Build(); + private static readonly IDeserializer Deserializer = + CommonDeserializerBuilder + .IgnoreUnmatchedProperties() + .Build(); + private static IDeserializer GetDeserializer(bool strict) => strict ? StrictDeserializer : Deserializer; private static readonly IValueSerializer Serializer = new SerializerBuilder() @@ -100,8 +106,9 @@ public void WriteYaml(IEmitter emitter, object value, Type type) /// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will /// be used. /// + /// true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise /// collection of objects - public static async Task> LoadAllFromStreamAsync(Stream stream, IDictionary typeMap = null) + public static async Task> LoadAllFromStreamAsync(Stream stream, IDictionary typeMap = null, bool strict = false) { var reader = new StreamReader(stream); var content = await reader.ReadToEndAsync().ConfigureAwait(false); @@ -117,8 +124,9 @@ public static async Task> LoadAllFromStreamAsync(Stream stream, IDi /// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will /// be used. /// + /// true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise /// collection of objects - public static async Task> LoadAllFromFileAsync(string fileName, IDictionary typeMap = null) + public static async Task> LoadAllFromFileAsync(string fileName, IDictionary typeMap = null, bool strict = false) { using (var fileStream = File.OpenRead(fileName)) { @@ -136,8 +144,9 @@ public static async Task> LoadAllFromFileAsync(string fileName, IDi /// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will /// be used. /// + /// true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise /// collection of objects - public static List LoadAllFromString(string content, IDictionary typeMap = null) + public static List LoadAllFromString(string content, IDictionary typeMap = null, bool strict = false) { var mergedTypeMap = new Dictionary(ModelTypeMap); // merge in KVPs from typeMap, overriding any in ModelTypeMap @@ -148,7 +157,7 @@ public static List LoadAllFromString(string content, IDictionary(); while (parser.Accept(out _)) { - var obj = Deserializer.Deserialize(parser); + var obj = GetDeserializer(strict).Deserialize(parser); types.Add(mergedTypeMap[obj.ApiVersion + "/" + obj.Kind]); } @@ -159,32 +168,32 @@ public static List LoadAllFromString(string content, IDictionary(out _)) { var objType = types[ix++]; - var obj = Deserializer.Deserialize(parser, objType); + var obj = GetDeserializer(strict).Deserialize(parser, objType); results.Add(obj); } return results; } - public static async Task LoadFromStreamAsync(Stream stream) + public static async Task LoadFromStreamAsync(Stream stream, bool strict = false) { var reader = new StreamReader(stream); var content = await reader.ReadToEndAsync().ConfigureAwait(false); - return Deserialize(content); + return Deserialize(content, strict); } - public static async Task LoadFromFileAsync(string file) + public static async Task LoadFromFileAsync(string file, bool strict = false) { using (var fs = File.OpenRead(file)) { - return await LoadFromStreamAsync(fs).ConfigureAwait(false); + return await LoadFromStreamAsync(fs, strict).ConfigureAwait(false); } } [Obsolete("use Deserialize")] - public static T LoadFromString(string content) + public static T LoadFromString(string content, bool strict = false) { - return Deserialize(content); + return Deserialize(content, strict); } [Obsolete("use Serialize")] @@ -193,14 +202,14 @@ public static string SaveToString(T value) return Serialize(value); } - public static TValue Deserialize(string yaml) + public static TValue Deserialize(string yaml, bool strict = false) { - return Deserializer.Deserialize(yaml); + return GetDeserializer(strict).Deserialize(yaml); } - public static TValue Deserialize(Stream yaml) + public static TValue Deserialize(Stream yaml, bool strict = false) { - return Deserializer.Deserialize(new StreamReader(yaml)); + return GetDeserializer(strict).Deserialize(new StreamReader(yaml)); } public static string SerializeAll(IEnumerable values) diff --git a/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs b/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs index 3205c071..7d23404e 100644 --- a/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs +++ b/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs @@ -540,6 +540,7 @@ public void LoadSameKubeConfigFromEnvironmentVariableUnmodified() public void LoadKubeConfigWithAdditionalProperties() { var txt = File.ReadAllText("assets/kubeconfig.additional-properties.yml"); + Assert.Throws(() => KubernetesYaml.Deserialize(txt, strict: true)); var expectedCfg = KubernetesYaml.Deserialize(txt); var fileInfo = new FileInfo(Path.GetFullPath("assets/kubeconfig.additional-properties.yml")); diff --git a/tests/KubernetesClient.Tests/KubernetesYamlTests.cs b/tests/KubernetesClient.Tests/KubernetesYamlTests.cs index 9e0482a1..43bffa8a 100644 --- a/tests/KubernetesClient.Tests/KubernetesYamlTests.cs +++ b/tests/KubernetesClient.Tests/KubernetesYamlTests.cs @@ -79,6 +79,7 @@ public void LoadAllFromStringWithAdditionalProperties() name: ns youDontKnow: Me"; + Assert.Throws(() => KubernetesYaml.LoadAllFromString(content, strict: true)); var objs = KubernetesYaml.LoadAllFromString(content); Assert.Equal(2, objs.Count); Assert.IsType(objs[0]); @@ -109,6 +110,7 @@ public void LoadAllFromStringWithAdditionalPropertiesAndTypes() name: ns youDontKnow: Me"; + Assert.Throws(() => KubernetesYaml.LoadAllFromString(content, strict: true)); var objs = KubernetesYaml.LoadAllFromString(content, types); Assert.Equal(2, objs.Count); Assert.IsType(objs[0]); @@ -218,6 +220,7 @@ public void LoadFromStringWithAdditionalProperties() var obj = KubernetesYaml.Deserialize(content); Assert.Equal("foo", obj.Metadata.Name); + Assert.Throws(() => KubernetesYaml.Deserialize(content, strict: true)); } [Fact] @@ -233,6 +236,7 @@ public void LoadFromStringWithAdditionalPropertiesAndCustomType() var obj = KubernetesYaml.Deserialize(content); Assert.Equal("foo", obj.Metadata.Name); + Assert.Throws(() => KubernetesYaml.Deserialize(content, strict: true)); } [Fact]