Skip to content

Commit 0b67950

Browse files
authored
Provide strict k8s yaml model deserializer (#1047)
* Provide strict k8s yaml model deserializer * Provide documentation * Add UT coverage
1 parent 6a7e9b2 commit 0b67950

File tree

3 files changed

+33
-19
lines changed

3 files changed

+33
-19
lines changed

src/KubernetesClient.Models/KubernetesYaml.cs

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,22 @@ namespace k8s
1515
/// </summary>
1616
public static class KubernetesYaml
1717
{
18-
private static readonly IDeserializer Deserializer =
18+
private static readonly DeserializerBuilder CommonDeserializerBuilder =
1919
new DeserializerBuilder()
2020
.WithNamingConvention(CamelCaseNamingConvention.Instance)
2121
.WithTypeConverter(new IntOrStringYamlConverter())
2222
.WithTypeConverter(new ByteArrayStringYamlConverter())
2323
.WithTypeConverter(new ResourceQuantityYamlConverter())
2424
.WithAttemptingUnquotedStringTypeDeserialization()
25-
.WithOverridesFromJsonPropertyAttributes()
26-
.IgnoreUnmatchedProperties()
27-
.Build();
25+
.WithOverridesFromJsonPropertyAttributes();
26+
private static readonly IDeserializer StrictDeserializer =
27+
CommonDeserializerBuilder
28+
.Build();
29+
private static readonly IDeserializer Deserializer =
30+
CommonDeserializerBuilder
31+
.IgnoreUnmatchedProperties()
32+
.Build();
33+
private static IDeserializer GetDeserializer(bool strict) => strict ? StrictDeserializer : Deserializer;
2834

2935
private static readonly IValueSerializer Serializer =
3036
new SerializerBuilder()
@@ -100,8 +106,9 @@ public void WriteYaml(IEmitter emitter, object value, Type type)
100106
/// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will
101107
/// be used.
102108
/// </param>
109+
/// <param name="strict">true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise</param>
103110
/// <returns>collection of objects</returns>
104-
public static async Task<List<object>> LoadAllFromStreamAsync(Stream stream, IDictionary<string, Type> typeMap = null)
111+
public static async Task<List<object>> LoadAllFromStreamAsync(Stream stream, IDictionary<string, Type> typeMap = null, bool strict = false)
105112
{
106113
var reader = new StreamReader(stream);
107114
var content = await reader.ReadToEndAsync().ConfigureAwait(false);
@@ -117,8 +124,9 @@ public static async Task<List<object>> LoadAllFromStreamAsync(Stream stream, IDi
117124
/// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will
118125
/// be used.
119126
/// </param>
127+
/// <param name="strict">true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise</param>
120128
/// <returns>collection of objects</returns>
121-
public static async Task<List<object>> LoadAllFromFileAsync(string fileName, IDictionary<string, Type> typeMap = null)
129+
public static async Task<List<object>> LoadAllFromFileAsync(string fileName, IDictionary<string, Type> typeMap = null, bool strict = false)
122130
{
123131
using (var fileStream = File.OpenRead(fileName))
124132
{
@@ -136,8 +144,9 @@ public static async Task<List<object>> LoadAllFromFileAsync(string fileName, IDi
136144
/// A map from apiVersion/kind to Type. For example "v1/Pod" -> typeof(V1Pod). If null, a default mapping will
137145
/// be used.
138146
/// </param>
147+
/// <param name="strict">true if a strict deserializer should be used (throwing exception on unknown properties), false otherwise</param>
139148
/// <returns>collection of objects</returns>
140-
public static List<object> LoadAllFromString(string content, IDictionary<string, Type> typeMap = null)
149+
public static List<object> LoadAllFromString(string content, IDictionary<string, Type> typeMap = null, bool strict = false)
141150
{
142151
var mergedTypeMap = new Dictionary<string, Type>(ModelTypeMap);
143152
// merge in KVPs from typeMap, overriding any in ModelTypeMap
@@ -148,7 +157,7 @@ public static List<object> LoadAllFromString(string content, IDictionary<string,
148157
parser.Consume<StreamStart>();
149158
while (parser.Accept<DocumentStart>(out _))
150159
{
151-
var obj = Deserializer.Deserialize<KubernetesObject>(parser);
160+
var obj = GetDeserializer(strict).Deserialize<KubernetesObject>(parser);
152161
types.Add(mergedTypeMap[obj.ApiVersion + "/" + obj.Kind]);
153162
}
154163

@@ -159,32 +168,32 @@ public static List<object> LoadAllFromString(string content, IDictionary<string,
159168
while (parser.Accept<DocumentStart>(out _))
160169
{
161170
var objType = types[ix++];
162-
var obj = Deserializer.Deserialize(parser, objType);
171+
var obj = GetDeserializer(strict).Deserialize(parser, objType);
163172
results.Add(obj);
164173
}
165174

166175
return results;
167176
}
168177

169-
public static async Task<T> LoadFromStreamAsync<T>(Stream stream)
178+
public static async Task<T> LoadFromStreamAsync<T>(Stream stream, bool strict = false)
170179
{
171180
var reader = new StreamReader(stream);
172181
var content = await reader.ReadToEndAsync().ConfigureAwait(false);
173-
return Deserialize<T>(content);
182+
return Deserialize<T>(content, strict);
174183
}
175184

176-
public static async Task<T> LoadFromFileAsync<T>(string file)
185+
public static async Task<T> LoadFromFileAsync<T>(string file, bool strict = false)
177186
{
178187
using (var fs = File.OpenRead(file))
179188
{
180-
return await LoadFromStreamAsync<T>(fs).ConfigureAwait(false);
189+
return await LoadFromStreamAsync<T>(fs, strict).ConfigureAwait(false);
181190
}
182191
}
183192

184193
[Obsolete("use Deserialize")]
185-
public static T LoadFromString<T>(string content)
194+
public static T LoadFromString<T>(string content, bool strict = false)
186195
{
187-
return Deserialize<T>(content);
196+
return Deserialize<T>(content, strict);
188197
}
189198

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

196-
public static TValue Deserialize<TValue>(string yaml)
205+
public static TValue Deserialize<TValue>(string yaml, bool strict = false)
197206
{
198-
return Deserializer.Deserialize<TValue>(yaml);
207+
return GetDeserializer(strict).Deserialize<TValue>(yaml);
199208
}
200209

201-
public static TValue Deserialize<TValue>(Stream yaml)
210+
public static TValue Deserialize<TValue>(Stream yaml, bool strict = false)
202211
{
203-
return Deserializer.Deserialize<TValue>(new StreamReader(yaml));
212+
return GetDeserializer(strict).Deserialize<TValue>(new StreamReader(yaml));
204213
}
205214

206215
public static string SerializeAll(IEnumerable<object> values)

tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@ public void LoadSameKubeConfigFromEnvironmentVariableUnmodified()
540540
public void LoadKubeConfigWithAdditionalProperties()
541541
{
542542
var txt = File.ReadAllText("assets/kubeconfig.additional-properties.yml");
543+
Assert.Throws<YamlDotNet.Core.YamlException>(() => KubernetesYaml.Deserialize<K8SConfiguration>(txt, strict: true));
543544
var expectedCfg = KubernetesYaml.Deserialize<K8SConfiguration>(txt);
544545

545546
var fileInfo = new FileInfo(Path.GetFullPath("assets/kubeconfig.additional-properties.yml"));

tests/KubernetesClient.Tests/KubernetesYamlTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public void LoadAllFromStringWithAdditionalProperties()
7979
name: ns
8080
youDontKnow: Me";
8181

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

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

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

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

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

238242
[Fact]

0 commit comments

Comments
 (0)