Skip to content

Commit 954b257

Browse files
authoredJun 26, 2023
feat: Adding the ability to inject CRD creation type overrides (#566)
Adding the ability to inject CRD creation type overrides into the container that will allow user defined CRD schema format This gives the user a greater degree of control over the CRD schema generation. Resolves #565
1 parent 67f158a commit 954b257

File tree

7 files changed

+271
-36
lines changed

7 files changed

+271
-36
lines changed
 

‎src/KubeOps/Operator/Builder/OperatorBuilder.cs

+3
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ internal IOperatorBuilder AddOperatorBase(OperatorSettings settings)
229229
Services.TryAddSingleton<ICrdBuilder, CrdBuilder>();
230230
Services.TryAddSingleton<IRbacBuilder, RbacBuilder>();
231231

232+
// Register type overrides for CRD generation
233+
Services.AddSingleton<ICrdBuilderTypeOverride, CrdBuilderResourceQuantityOverride>();
234+
232235
return this;
233236
}
234237
}

‎src/KubeOps/Operator/Entities/CrdBuilder.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ namespace KubeOps.Operator.Entities;
1010
internal class CrdBuilder : ICrdBuilder
1111
{
1212
private readonly IComponentRegistrar _componentRegistrar;
13+
private readonly IList<ICrdBuilderTypeOverride>? _crdBuilderOverrides;
1314

14-
public CrdBuilder(IComponentRegistrar componentRegistrar)
15+
public CrdBuilder(IComponentRegistrar componentRegistrar, IEnumerable<ICrdBuilderTypeOverride>? crdBuilderOverrides = null)
1516
{
1617
_componentRegistrar = componentRegistrar;
18+
_crdBuilderOverrides = crdBuilderOverrides?.ToList();
1719
}
1820

1921
public IEnumerable<V1CustomResourceDefinition> BuildCrds() =>
@@ -22,7 +24,7 @@ public IEnumerable<V1CustomResourceDefinition> BuildCrds() =>
2224
.Where(type => type.Assembly != typeof(KubernetesEntityAttribute).Assembly)
2325
.Where(type => type.GetCustomAttributes<KubernetesEntityAttribute>().Any())
2426
.Where(type => !type.GetCustomAttributes<IgnoreEntityAttribute>().Any())
25-
.Select(type => (type.CreateCrd(), type.GetCustomAttributes<StorageVersionAttribute>().Any()))
27+
.Select(type => (type.CreateCrd(_crdBuilderOverrides), type.GetCustomAttributes<StorageVersionAttribute>().Any()))
2628
.GroupBy(grp => grp.Item1.Metadata.Name)
2729
.Select(
2830
group =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using k8s.Models;
2+
3+
namespace KubeOps.Operator.Entities;
4+
5+
/// <summary>
6+
/// Custom CRD schema creation override to resolve https://github.com/buehler/dotnet-operator-sdk/issues/565.
7+
/// </summary>
8+
public class CrdBuilderResourceQuantityOverride : ICrdBuilderTypeOverride
9+
{
10+
public bool HandlesType(Type type) => type.IsGenericType
11+
&& type.GetGenericTypeDefinition() == typeof(IDictionary<,>)
12+
&& type.GenericTypeArguments.Contains(typeof(ResourceQuantity));
13+
14+
public void ConfigureCustomSchemaForProp(V1JSONSchemaProps props)
15+
{
16+
props.Type = "object";
17+
props.XKubernetesPreserveUnknownFields = true;
18+
}
19+
}

‎src/KubeOps/Operator/Entities/Extensions/EntityToCrdExtensions.cs

+26-16
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ internal static class EntityToCrdExtensions
3030
private static readonly string[] IgnoredToplevelProperties = { "metadata", "apiversion", "kind" };
3131

3232
internal static V1CustomResourceDefinition CreateCrd(
33-
this IKubernetesObject<V1ObjectMeta> kubernetesEntity) => CreateCrd(kubernetesEntity.GetType());
33+
this IKubernetesObject<V1ObjectMeta> kubernetesEntity, IList<ICrdBuilderTypeOverride>? crdBuilderOverrides = null)
34+
=> CreateCrd(kubernetesEntity.GetType(), crdBuilderOverrides);
3435

35-
internal static V1CustomResourceDefinition CreateCrd<TEntity>()
36-
where TEntity : IKubernetesObject<V1ObjectMeta> => CreateCrd(typeof(TEntity));
36+
internal static V1CustomResourceDefinition CreateCrd<TEntity>(IList<ICrdBuilderTypeOverride>? crdBuilderOverrides = null)
37+
where TEntity : IKubernetesObject<V1ObjectMeta> => CreateCrd(typeof(TEntity), crdBuilderOverrides);
3738

38-
internal static V1CustomResourceDefinition CreateCrd(this Type entityType)
39+
internal static V1CustomResourceDefinition CreateCrd(this Type entityType, IList<ICrdBuilderTypeOverride>? crdBuilderOverrides = null)
3940
{
4041
var entityDefinition = entityType.ToEntityDefinition();
4142

@@ -79,7 +80,7 @@ internal static V1CustomResourceDefinition CreateCrd(this Type entityType)
7980
}
8081

8182
var columns = new List<V1CustomResourceColumnDefinition>();
82-
version.Schema = new V1CustomResourceValidation(MapType(entityType, columns, string.Empty));
83+
version.Schema = new V1CustomResourceValidation(MapType(entityType, columns, string.Empty, crdBuilderOverrides));
8384

8485
version.AdditionalPrinterColumns = entityType
8586
.GetCustomAttributes<GenericAdditionalPrinterColumnAttribute>(true)
@@ -98,12 +99,13 @@ internal static V1CustomResourceDefinition CreateCrd(this Type entityType)
9899
private static V1JSONSchemaProps MapProperty(
99100
PropertyInfo info,
100101
IList<V1CustomResourceColumnDefinition> additionalColumns,
101-
string jsonPath)
102+
string jsonPath,
103+
IList<ICrdBuilderTypeOverride>? crdBuilderOverrides = null)
102104
{
103105
V1JSONSchemaProps props;
104106
try
105107
{
106-
props = MapType(info.PropertyType, additionalColumns, jsonPath);
108+
props = MapType(info.PropertyType, additionalColumns, jsonPath, crdBuilderOverrides);
107109
}
108110
catch (Exception ex)
109111
{
@@ -214,7 +216,8 @@ private static V1JSONSchemaProps MapProperty(
214216
private static V1JSONSchemaProps MapType(
215217
Type type,
216218
IList<V1CustomResourceColumnDefinition> additionalColumns,
217-
string jsonPath)
219+
string jsonPath,
220+
IList<ICrdBuilderTypeOverride>? crdBuilderOverrides = null)
218221
{
219222
var props = new V1JSONSchemaProps();
220223

@@ -223,7 +226,12 @@ private static V1JSONSchemaProps MapType(
223226

224227
var isSimpleType = IsSimpleType(type);
225228

226-
if (type == typeof(V1ObjectMeta))
229+
var matchedOverride = crdBuilderOverrides?.FirstOrDefault(ovrd => ovrd.HandlesType(type));
230+
if (matchedOverride != null)
231+
{
232+
matchedOverride.ConfigureCustomSchemaForProp(props);
233+
}
234+
else if (type == typeof(V1ObjectMeta))
227235
{
228236
props.Type = Object;
229237
}
@@ -233,13 +241,14 @@ private static V1JSONSchemaProps MapType(
233241
props.Items = MapType(
234242
type.GetElementType() ?? throw new NullReferenceException("No Array Element Type found"),
235243
additionalColumns,
236-
jsonPath);
244+
jsonPath,
245+
crdBuilderOverrides);
237246
}
238247
else if (!isSimpleType && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IDictionary<,>))
239248
{
240249
var genericTypes = type.GenericTypeArguments;
241250
props.Type = Object;
242-
props.AdditionalProperties = MapType(genericTypes[1], additionalColumns, jsonPath);
251+
props.AdditionalProperties = MapType(genericTypes[1], additionalColumns, jsonPath, crdBuilderOverrides);
243252
}
244253
else if (!isSimpleType &&
245254
type.IsGenericType &&
@@ -250,7 +259,7 @@ private static V1JSONSchemaProps MapType(
250259
{
251260
var genericTypes = type.GenericTypeArguments.Single().GenericTypeArguments;
252261
props.Type = Object;
253-
props.AdditionalProperties = MapType(genericTypes[1], additionalColumns, jsonPath);
262+
props.AdditionalProperties = MapType(genericTypes[1], additionalColumns, jsonPath, crdBuilderOverrides);
254263
}
255264
else if (!isSimpleType &&
256265
(typeof(IDictionary).IsAssignableFrom(type) ||
@@ -265,7 +274,7 @@ private static V1JSONSchemaProps MapType(
265274
else if (!isSimpleType && IsGenericEnumerableType(type, out Type? closingType))
266275
{
267276
props.Type = Array;
268-
props.Items = MapType(closingType, additionalColumns, jsonPath);
277+
props.Items = MapType(closingType, additionalColumns, jsonPath, crdBuilderOverrides);
269278
}
270279
else if (type == typeof(IntstrIntOrString))
271280
{
@@ -280,7 +289,7 @@ private static V1JSONSchemaProps MapType(
280289
}
281290
else if (!isSimpleType)
282291
{
283-
ProcessType(type, props, additionalColumns, jsonPath);
292+
ProcessType(type, props, additionalColumns, jsonPath, crdBuilderOverrides);
284293
}
285294
else if (type == typeof(int) || Nullable.GetUnderlyingType(type) == typeof(int))
286295
{
@@ -345,7 +354,8 @@ private static void ProcessType(
345354
Type type,
346355
V1JSONSchemaProps props,
347356
IList<V1CustomResourceColumnDefinition> additionalColumns,
348-
string jsonPath)
357+
string jsonPath,
358+
IList<ICrdBuilderTypeOverride>? crdBuilderOverrides = null)
349359
{
350360
props.Type = Object;
351361

@@ -358,7 +368,7 @@ private static void ProcessType(
358368
.Select(
359369
prop => KeyValuePair.Create(
360370
GetPropertyName(prop),
361-
MapProperty(prop, additionalColumns, $"{jsonPath}.{GetPropertyName(prop)}"))));
371+
MapProperty(prop, additionalColumns, $"{jsonPath}.{GetPropertyName(prop)}", crdBuilderOverrides))));
362372
props.Required = type.GetProperties()
363373
.Where(
364374
prop => prop.GetCustomAttribute<RequiredAttribute>() != null &&
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using k8s.Models;
2+
3+
namespace KubeOps.Operator.Entities;
4+
5+
/// <summary>
6+
/// The override definition class which sets the condition for which the type should be overridden during CRD generation,
7+
/// and what should the serialized values map to.
8+
/// </summary>
9+
public interface ICrdBuilderTypeOverride
10+
{
11+
/// <summary>
12+
/// Checks if the type matches the user defined condition that will custom configure the schema property for the given type.
13+
/// </summary>
14+
/// <param name="type">Type being checked against.</param>
15+
/// <returns>Boolean determining whether the user defined type condition has been matched.</returns>
16+
public bool HandlesType(Type type);
17+
18+
/// <summary>
19+
/// For the matching condition, configure the CRD property in a user defined way for the given type.
20+
/// </summary>
21+
/// <param name="props">The object type that will be converted to a schema.</param>
22+
public void ConfigureCustomSchemaForProp(V1JSONSchemaProps props);
23+
}

‎tests/KubeOps.Test/Operator/Generators/CrdGenerator.Test.cs

+51-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using FluentAssertions;
1+
using System.Reflection;
2+
using FluentAssertions;
3+
using k8s;
24
using k8s.Models;
35
using KubeOps.Operator.Builder;
46
using KubeOps.Operator.Entities;
@@ -10,34 +12,34 @@ namespace KubeOps.Test.Operator.Generators;
1012
public class CrdGeneratorTest
1113
{
1214
private readonly IEnumerable<V1CustomResourceDefinition> _crds;
15+
private readonly ComponentRegistrar _componentRegistrar = new();
1316

1417
public CrdGeneratorTest()
1518
{
16-
var componentRegistrar = new ComponentRegistrar();
17-
18-
componentRegistrar.RegisterEntity<TestIgnoredEntity>();
19-
componentRegistrar.RegisterEntity<TestInvalidEntity>();
20-
componentRegistrar.RegisterEntity<TestSpecEntity>();
21-
componentRegistrar.RegisterEntity<TestClusterSpecEntity>();
22-
componentRegistrar.RegisterEntity<TestStatusEntity>();
23-
componentRegistrar.RegisterEntity<V1Alpha1VersionedEntity>();
24-
componentRegistrar.RegisterEntity<V1AttributeVersionedEntity>();
25-
componentRegistrar.RegisterEntity<V1Beta1VersionedEntity>();
26-
componentRegistrar.RegisterEntity<V1VersionedEntity>();
27-
componentRegistrar.RegisterEntity<V2AttributeVersionedEntity>();
28-
componentRegistrar.RegisterEntity<V2Beta2VersionedEntity>();
29-
componentRegistrar.RegisterEntity<V2VersionedEntity>();
19+
_componentRegistrar.RegisterEntity<TestIgnoredEntity>();
20+
_componentRegistrar.RegisterEntity<TestInvalidEntity>();
21+
_componentRegistrar.RegisterEntity<TestSpecEntity>();
22+
_componentRegistrar.RegisterEntity<TestClusterSpecEntity>();
23+
_componentRegistrar.RegisterEntity<TestStatusEntity>();
24+
_componentRegistrar.RegisterEntity<V1Alpha1VersionedEntity>();
25+
_componentRegistrar.RegisterEntity<V1AttributeVersionedEntity>();
26+
_componentRegistrar.RegisterEntity<V1Beta1VersionedEntity>();
27+
_componentRegistrar.RegisterEntity<V1VersionedEntity>();
28+
_componentRegistrar.RegisterEntity<V2AttributeVersionedEntity>();
29+
_componentRegistrar.RegisterEntity<V2Beta2VersionedEntity>();
30+
_componentRegistrar.RegisterEntity<V2VersionedEntity>();
31+
_componentRegistrar.RegisterEntity<TestCustomCrdTypeOverrides>();
3032

3133
// Should be ignored since V1Pod is from the k8s assembly.
32-
componentRegistrar.RegisterEntity<V1Pod>();
34+
_componentRegistrar.RegisterEntity<V1Pod>();
3335

34-
_crds = new CrdBuilder(componentRegistrar).BuildCrds();
36+
_crds = new CrdBuilder(_componentRegistrar).BuildCrds();
3537
}
3638

3739
[Fact]
3840
public void Should_Generate_Correct_Number_Of_Crds()
3941
{
40-
_crds.Count().Should().Be(5);
42+
_crds.Count().Should().Be(6);
4143
}
4244

4345
[Fact]
@@ -101,4 +103,35 @@ public void Should_Add_ShortNames_To_Crd()
101103
.And
102104
.Contain(new[] { "foo", "bar", "baz" });
103105
}
106+
107+
[Fact]
108+
public void Should_Create_Crd_As_Default_Without_Crd_Type_Overrides()
109+
{
110+
var crdWithoutOverrides = new CrdBuilder(_componentRegistrar)
111+
.BuildCrds()
112+
.First(
113+
114+
c => c.Spec.Names.Kind.Contains("testcustomtypeoverrides", StringComparison.InvariantCultureIgnoreCase));
115+
116+
117+
var serializedWithoutOverrides = TestTypeOverridesValues.SerializeWithoutDescriptions(crdWithoutOverrides);
118+
119+
serializedWithoutOverrides.Should().Contain(TestTypeOverridesValues.ExpectedDefaultYamlResources);
120+
serializedWithoutOverrides.Should().NotContain(TestTypeOverridesValues.ExpectedOverriddenResourcesYaml);
121+
}
122+
123+
[Fact]
124+
public void Should_Convert_Desired_Crd_Type_Everywhere_To_Desired_Crd_Format()
125+
{
126+
var customOverrides = new List<ICrdBuilderTypeOverride> { new CrdBuilderResourceQuantityOverride() };
127+
var crdWithTypeOverrides = new CrdBuilder(_componentRegistrar, customOverrides)
128+
.BuildCrds()
129+
.First(
130+
c => c.Spec.Names.Kind.Contains("testcustomtypeoverrides", StringComparison.InvariantCultureIgnoreCase));
131+
var serializedWithOverrides = TestTypeOverridesValues.SerializeWithoutDescriptions(crdWithTypeOverrides);
132+
133+
serializedWithOverrides.Should().Contain(TestTypeOverridesValues.ExpectedOverriddenResourcesYaml);
134+
serializedWithOverrides.Should().NotContain(TestTypeOverridesValues.ExpectedDefaultYamlResources);
135+
136+
}
104137
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System.IdentityModel.Tokens.Jwt;
2+
using System.Text;
3+
using k8s;
4+
using k8s.Models;
5+
using KubeOps.Operator.Entities;
6+
using YamlDotNet.RepresentationModel;
7+
8+
namespace KubeOps.Test.TestEntities;
9+
10+
public class TestStatus
11+
{
12+
public string SpecString { get; set; } = string.Empty;
13+
}
14+
15+
public class TestResourceRequirements
16+
{
17+
public V1ResourceRequirements Resources { get; set; } = new();
18+
}
19+
20+
public class TestSpec
21+
{
22+
public TestResourceRequirements TestResourceRequirements { get; set; } = new();
23+
public DateTime LastModified { get; set; }
24+
public V1ResourceRequirements AnotherResourceRequirements { get; set; } = new();
25+
}
26+
27+
[KubernetesEntity(
28+
ApiVersion = "v1",
29+
Kind = "TestCustomTypeOverrides",
30+
Group = "kubeops.test.dev",
31+
PluralName = "testcustomtypeoverrides")]
32+
public class TestCustomCrdTypeOverrides : CustomKubernetesEntity<TestSpec, TestStatus>
33+
{
34+
35+
36+
}
37+
38+
public static class TestTypeOverridesValues
39+
{
40+
public static string SerializeWithoutDescriptions(V1CustomResourceDefinition? resource)
41+
{
42+
var yamlText = KubernetesYaml.Serialize(resource);
43+
var yaml = new YamlStream();
44+
yaml.Load(new StringReader(yamlText));
45+
46+
var mapping = (YamlMappingNode)yaml.Documents[0].RootNode;
47+
RemoveDescriptionFromYaml(mapping);
48+
49+
var stringWriter = new StringWriter();
50+
yaml.Save(stringWriter, false);
51+
52+
var updatedYamlText = stringWriter.ToString();
53+
return updatedYamlText;
54+
55+
}
56+
57+
/// <summary>
58+
/// Recursively removes all yaml keys value pairs named "description".
59+
/// </summary>
60+
/// <param name="node"></param>
61+
private static void RemoveDescriptionFromYaml(YamlNode node)
62+
{
63+
switch (node)
64+
{
65+
case YamlMappingNode mapping:
66+
{
67+
var nodesToRemove = new List<YamlNode>();
68+
69+
foreach (var entry in mapping.Children)
70+
{
71+
if (entry.Key.ToString() == "description")
72+
{
73+
nodesToRemove.Add(entry.Key);
74+
}
75+
else
76+
{
77+
RemoveDescriptionFromYaml(entry.Value);
78+
}
79+
}
80+
81+
foreach (var key in nodesToRemove)
82+
{
83+
mapping.Children.Remove(key);
84+
}
85+
86+
break;
87+
}
88+
case YamlSequenceNode sequence:
89+
{
90+
foreach (var child in sequence.Children)
91+
{
92+
RemoveDescriptionFromYaml(child);
93+
}
94+
95+
break;
96+
}
97+
}
98+
}
99+
100+
101+
public const string ExpectedOverriddenResourcesYaml = @"
102+
limits:
103+
type: object
104+
x-kubernetes-preserve-unknown-fields: true
105+
requests:
106+
type: object
107+
x-kubernetes-preserve-unknown-fields: true";
108+
109+
public const string ExpectedDefaultYamlResources = @"
110+
resources:
111+
properties:
112+
claims:
113+
items:
114+
properties:
115+
name:
116+
type: string
117+
type: object
118+
type: array
119+
limits:
120+
additionalProperties:
121+
properties:
122+
format:
123+
enum:
124+
- DecimalExponent
125+
- BinarySI
126+
- DecimalSI
127+
type: string
128+
value:
129+
type: string
130+
type: object
131+
type: object
132+
requests:
133+
additionalProperties:
134+
properties:
135+
format:
136+
enum:
137+
- DecimalExponent
138+
- BinarySI
139+
- DecimalSI
140+
type: string
141+
value:
142+
type: string
143+
type: object
144+
type: object";
145+
}

0 commit comments

Comments
 (0)
Please sign in to comment.