Skip to content

Commit

Permalink
Provide strict k8s yaml model deserializer (#1047)
Browse files Browse the repository at this point in the history
* Provide strict k8s yaml model deserializer

* Provide documentation

* Add UT coverage
  • Loading branch information
stan-sz authored Oct 14, 2022
1 parent 6a7e9b2 commit 0b67950
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 19 deletions.
47 changes: 28 additions & 19 deletions src/KubernetesClient.Models/KubernetesYaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,22 @@ namespace k8s
/// </summary>
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()
Expand Down Expand Up @@ -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.
/// </param>
/// <param name="strict">true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise</param>
/// <returns>collection of objects</returns>
public static async Task<List<object>> LoadAllFromStreamAsync(Stream stream, IDictionary<string, Type> typeMap = null)
public static async Task<List<object>> LoadAllFromStreamAsync(Stream stream, IDictionary<string, Type> typeMap = null, bool strict = false)
{
var reader = new StreamReader(stream);
var content = await reader.ReadToEndAsync().ConfigureAwait(false);
Expand All @@ -117,8 +124,9 @@ public static async Task<List<object>> 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.
/// </param>
/// <param name="strict">true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise</param>
/// <returns>collection of objects</returns>
public static async Task<List<object>> LoadAllFromFileAsync(string fileName, IDictionary<string, Type> typeMap = null)
public static async Task<List<object>> LoadAllFromFileAsync(string fileName, IDictionary<string, Type> typeMap = null, bool strict = false)
{
using (var fileStream = File.OpenRead(fileName))
{
Expand All @@ -136,8 +144,9 @@ public static async Task<List<object>> 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.
/// </param>
/// <param name="strict">true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise</param>
/// <returns>collection of objects</returns>
public static List<object> LoadAllFromString(string content, IDictionary<string, Type> typeMap = null)
public static List<object> LoadAllFromString(string content, IDictionary<string, Type> typeMap = null, bool strict = false)
{
var mergedTypeMap = new Dictionary<string, Type>(ModelTypeMap);
// merge in KVPs from typeMap, overriding any in ModelTypeMap
Expand All @@ -148,7 +157,7 @@ public static List<object> LoadAllFromString(string content, IDictionary<string,
parser.Consume<StreamStart>();
while (parser.Accept<DocumentStart>(out _))
{
var obj = Deserializer.Deserialize<KubernetesObject>(parser);
var obj = GetDeserializer(strict).Deserialize<KubernetesObject>(parser);
types.Add(mergedTypeMap[obj.ApiVersion + "/" + obj.Kind]);
}

Expand All @@ -159,32 +168,32 @@ public static List<object> LoadAllFromString(string content, IDictionary<string,
while (parser.Accept<DocumentStart>(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<T> LoadFromStreamAsync<T>(Stream stream)
public static async Task<T> LoadFromStreamAsync<T>(Stream stream, bool strict = false)
{
var reader = new StreamReader(stream);
var content = await reader.ReadToEndAsync().ConfigureAwait(false);
return Deserialize<T>(content);
return Deserialize<T>(content, strict);
}

public static async Task<T> LoadFromFileAsync<T>(string file)
public static async Task<T> LoadFromFileAsync<T>(string file, bool strict = false)
{
using (var fs = File.OpenRead(file))
{
return await LoadFromStreamAsync<T>(fs).ConfigureAwait(false);
return await LoadFromStreamAsync<T>(fs, strict).ConfigureAwait(false);
}
}

[Obsolete("use Deserialize")]
public static T LoadFromString<T>(string content)
public static T LoadFromString<T>(string content, bool strict = false)
{
return Deserialize<T>(content);
return Deserialize<T>(content, strict);
}

[Obsolete("use Serialize")]
Expand All @@ -193,14 +202,14 @@ public static string SaveToString<T>(T value)
return Serialize(value);
}

public static TValue Deserialize<TValue>(string yaml)
public static TValue Deserialize<TValue>(string yaml, bool strict = false)
{
return Deserializer.Deserialize<TValue>(yaml);
return GetDeserializer(strict).Deserialize<TValue>(yaml);
}

public static TValue Deserialize<TValue>(Stream yaml)
public static TValue Deserialize<TValue>(Stream yaml, bool strict = false)
{
return Deserializer.Deserialize<TValue>(new StreamReader(yaml));
return GetDeserializer(strict).Deserialize<TValue>(new StreamReader(yaml));
}

public static string SerializeAll(IEnumerable<object> values)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ public void LoadSameKubeConfigFromEnvironmentVariableUnmodified()
public void LoadKubeConfigWithAdditionalProperties()
{
var txt = File.ReadAllText("assets/kubeconfig.additional-properties.yml");
Assert.Throws<YamlDotNet.Core.YamlException>(() => KubernetesYaml.Deserialize<K8SConfiguration>(txt, strict: true));
var expectedCfg = KubernetesYaml.Deserialize<K8SConfiguration>(txt);

var fileInfo = new FileInfo(Path.GetFullPath("assets/kubeconfig.additional-properties.yml"));
Expand Down
4 changes: 4 additions & 0 deletions tests/KubernetesClient.Tests/KubernetesYamlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public void LoadAllFromStringWithAdditionalProperties()
name: ns
youDontKnow: Me";

Assert.Throws<YamlDotNet.Core.YamlException>(() => KubernetesYaml.LoadAllFromString(content, strict: true));
var objs = KubernetesYaml.LoadAllFromString(content);
Assert.Equal(2, objs.Count);
Assert.IsType<V1Pod>(objs[0]);
Expand Down Expand Up @@ -109,6 +110,7 @@ public void LoadAllFromStringWithAdditionalPropertiesAndTypes()
name: ns
youDontKnow: Me";

Assert.Throws<YamlDotNet.Core.YamlException>(() => KubernetesYaml.LoadAllFromString(content, strict: true));
var objs = KubernetesYaml.LoadAllFromString(content, types);
Assert.Equal(2, objs.Count);
Assert.IsType<MyPod>(objs[0]);
Expand Down Expand Up @@ -218,6 +220,7 @@ public void LoadFromStringWithAdditionalProperties()
var obj = KubernetesYaml.Deserialize<V1Pod>(content);

Assert.Equal("foo", obj.Metadata.Name);
Assert.Throws<YamlDotNet.Core.YamlException>(() => KubernetesYaml.Deserialize<V1Pod>(content, strict: true));
}

[Fact]
Expand All @@ -233,6 +236,7 @@ public void LoadFromStringWithAdditionalPropertiesAndCustomType()
var obj = KubernetesYaml.Deserialize<V1Pod>(content);

Assert.Equal("foo", obj.Metadata.Name);
Assert.Throws<YamlDotNet.Core.YamlException>(() => KubernetesYaml.Deserialize<V1Pod>(content, strict: true));
}

[Fact]
Expand Down

0 comments on commit 0b67950

Please sign in to comment.