Skip to content

Commit 690c938

Browse files
author
Christoph Bühler
committed
feat(crd generation): add validators
this closes #8. added various validation attributes for the crd generation. described in the readme.
1 parent ae96986 commit 690c938

19 files changed

+751
-182
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ public class Foo : CustomKubernetesEntity<FooSpec>
6868

6969
Now a CRD for your "Foo" class is generated on build.
7070

71+
#### Validation
72+
73+
You can use the various validator attributes to customize your crd:
74+
75+
(all attributes are on properties with the exception of the Description)
76+
77+
- `Description`: Describe the property or class
78+
- `ExternalDocs`: Add a link to an external documentation
79+
- `Items`: Customize MinItems / MaxItems and if the items should be unique
80+
- `Lenght`: Customize the length of something
81+
- `MultipleOf`: A number should be a multiple of
82+
- `Pattern`: A valid ECMA script regex (e.g. `/\d*/`)
83+
- `RangeMaximum`: The maximum of a value (with option to exclude the max itself)
84+
- `RangeMinimum`: The minimum of a value (with option to exclude the min itself)
85+
- `Required`: The field is listed in the required fields
86+
7187
### Write Controllers
7288

7389
```csharp

src/KubeOps/KubeOps.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<PackageReference Include="KubernetesClient" Version="2.0.21" />
2929
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="3.0.0" />
3030
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine" Version="3.0.0" />
31+
<PackageReference Include="Namotion.Reflection" Version="1.0.11" />
3132
<PackageReference Include="YamlDotNet" Version="8.1.1" />
3233
</ItemGroup>
3334

@@ -40,4 +41,10 @@
4041
</Content>
4142
</ItemGroup>
4243

44+
<ItemGroup>
45+
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
46+
<_Parameter1>$(MSBuildProjectName).Test</_Parameter1>
47+
</AssemblyAttribute>
48+
</ItemGroup>
49+
4350
</Project>
Lines changed: 20 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.ComponentModel.DataAnnotations;
43
using System.IO;
54
using System.Linq;
65
using System.Reflection;
76
using System.Text;
87
using System.Threading.Tasks;
98
using k8s.Models;
10-
using KubeOps.Operator.Entities;
119
using KubeOps.Operator.Entities.Extensions;
1210
using KubeOps.Operator.Entities.Kustomize;
1311
using KubeOps.Operator.Serialization;
@@ -18,18 +16,6 @@ namespace KubeOps.Operator.Commands.Generators
1816
[Command("crd", "crds", Description = "Generates the needed CRD for kubernetes.")]
1917
internal class CrdGenerator : GeneratorBase
2018
{
21-
private const string Integer = "integer";
22-
private const string Number = "number";
23-
private const string String = "string";
24-
private const string Boolean = "boolean";
25-
private const string Object = "object";
26-
27-
private const string Int32 = "int32";
28-
private const string Int64 = "int64";
29-
private const string Float = "float";
30-
private const string Double = "double";
31-
private const string DateTime = "date-time";
32-
3319
private readonly EntitySerializer _serializer;
3420

3521
public CrdGenerator(EntitySerializer serializer)
@@ -47,15 +33,19 @@ public async Task<int> OnExecuteAsync(CommandLineApplication app)
4733
{
4834
Directory.CreateDirectory(OutputPath);
4935

50-
var kustomizeOutput = Encoding.UTF8.GetBytes(_serializer.Serialize(new KustomizationConfig
51-
{
52-
Resources = crds
53-
.Select(crd => $"{crd.Metadata.Name.Replace('.', '_')}.{Format.ToString().ToLower()}").ToList(),
54-
CommonLabels = new Dictionary<string, string>
55-
{
56-
{"operator-element", "crd"},
57-
},
58-
}, Format));
36+
var kustomizeOutput = Encoding.UTF8.GetBytes(
37+
_serializer.Serialize(
38+
new KustomizationConfig
39+
{
40+
Resources = crds
41+
.Select(crd => $"{crd.Metadata.Name.Replace('.', '_')}.{Format.ToString().ToLower()}")
42+
.ToList(),
43+
CommonLabels = new Dictionary<string, string>
44+
{
45+
{ "operator-element", "crd" },
46+
},
47+
},
48+
Format));
5949
await using var kustomizationFile =
6050
File.Open(Path.Join(OutputPath, $"kustomization.{Format.ToString().ToLower()}"), FileMode.Create);
6151
await kustomizationFile.WriteAsync(kustomizeOutput);
@@ -69,8 +59,11 @@ public async Task<int> OnExecuteAsync(CommandLineApplication app)
6959

7060
if (!string.IsNullOrWhiteSpace(OutputPath))
7161
{
72-
await using var file = File.Open(Path.Join(OutputPath,
73-
$"{crd.Metadata.Name.Replace('.', '_')}.{Format.ToString().ToLower()}"), FileMode.Create);
62+
await using var file = File.Open(
63+
Path.Join(
64+
OutputPath,
65+
$"{crd.Metadata.Name.Replace('.', '_')}.{Format.ToString().ToLower()}"),
66+
FileMode.Create);
7467
await file.WriteAsync(Encoding.UTF8.GetBytes(output));
7568
}
7669
else
@@ -90,151 +83,12 @@ public static IEnumerable<V1CustomResourceDefinition> GenerateCrds()
9083
throw new Exception("No Entry Assembly found.");
9184
}
9285

93-
var result = new List<V1CustomResourceDefinition>();
94-
foreach (var entityType in GetTypesWithAttribute<KubernetesEntityAttribute>(assembly))
95-
{
96-
var entityDefinition = CustomEntityDefinitionExtensions.CreateResourceDefinition(entityType);
97-
98-
var crd = new V1CustomResourceDefinition(
99-
new V1CustomResourceDefinitionSpec(),
100-
$"{V1CustomResourceDefinition.KubeGroup}/{V1CustomResourceDefinition.KubeApiVersion}",
101-
V1CustomResourceDefinition.KubeKind,
102-
new V1ObjectMeta {Name = $"{entityDefinition.Plural}.{entityDefinition.Group}"});
103-
104-
var spec = crd.Spec;
105-
spec.Group = entityDefinition.Group;
106-
spec.Names = new V1CustomResourceDefinitionNames
107-
{
108-
Kind = entityDefinition.Kind,
109-
ListKind = entityDefinition.ListKind,
110-
Singular = entityDefinition.Singular,
111-
Plural = entityDefinition.Plural,
112-
};
113-
spec.Scope = entityDefinition.Scope.ToString();
114-
115-
var version = new V1CustomResourceDefinitionVersion();
116-
spec.Versions = new[] {version};
117-
118-
// TODO: versions?
119-
version.Name = entityDefinition.Version;
120-
version.Served = true;
121-
version.Storage = true;
122-
123-
if (entityType.GetProperty("Status") != null)
124-
{
125-
version.Subresources = new V1CustomResourceSubresources(null, new { });
126-
}
127-
128-
version.Schema = new V1CustomResourceValidation(MapType(entityType));
129-
130-
result.Add(crd);
131-
}
132-
133-
return result;
134-
}
135-
136-
private static V1JSONSchemaProps MapProperty(PropertyInfo info)
137-
{
138-
// TODO: get description somehow.
139-
// TODO: support description via XML fields -> but describe how (generate xml file stuff)
140-
141-
var props = MapType(info.PropertyType);
142-
props.Description ??= info.GetCustomAttribute<DisplayAttribute>()?.Description;
143-
return props;
144-
}
145-
146-
private static V1JSONSchemaProps MapType(Type type)
147-
{
148-
var props = new V1JSONSchemaProps();
149-
150-
// this description is on the class
151-
props.Description = type.GetCustomAttributes<DisplayAttribute>(true).FirstOrDefault()?.Description;
152-
153-
if (type == typeof(V1ObjectMeta))
154-
{
155-
// TODO(check): is this correct? should metadata be filtered?
156-
props.Type = Object;
157-
}
158-
else if (!IsSimpleType(type))
159-
{
160-
props.Type = Object;
161-
props.Properties = new Dictionary<string, V1JSONSchemaProps>(
162-
type.GetProperties()
163-
.Select(
164-
prop => KeyValuePair.Create(
165-
CamelCase(prop.Name),
166-
MapProperty(prop))));
167-
}
168-
else if (type == typeof(int) || Nullable.GetUnderlyingType(type) == typeof(int))
169-
{
170-
props.Type = Integer;
171-
props.Format = Int32;
172-
}
173-
else if (type == typeof(long) || Nullable.GetUnderlyingType(type) == typeof(long))
174-
{
175-
props.Type = Integer;
176-
props.Format = Int64;
177-
}
178-
else if (type == typeof(float) || Nullable.GetUnderlyingType(type) == typeof(float))
179-
{
180-
props.Type = Number;
181-
props.Format = Float;
182-
}
183-
else if (type == typeof(double) || Nullable.GetUnderlyingType(type) == typeof(double))
184-
{
185-
props.Type = Number;
186-
props.Format = Double;
187-
}
188-
else if (type == typeof(string) || Nullable.GetUnderlyingType(type) == typeof(string))
189-
{
190-
props.Type = String;
191-
}
192-
else if (type == typeof(bool) || Nullable.GetUnderlyingType(type) == typeof(bool))
193-
{
194-
props.Type = Boolean;
195-
}
196-
else if (type == typeof(DateTime) || Nullable.GetUnderlyingType(type) == typeof(DateTime))
197-
{
198-
props.Type = String;
199-
props.Format = DateTime;
200-
}
201-
else if (type.IsEnum)
202-
{
203-
props.Type = String;
204-
props.EnumProperty = new List<object>(Enum.GetNames(type));
205-
}
206-
207-
if (Nullable.GetUnderlyingType(type) != null)
208-
{
209-
props.Nullable = true;
210-
}
211-
212-
// TODO: validator attributes
213-
214-
return props;
86+
return GetTypesWithAttribute<KubernetesEntityAttribute>(assembly)
87+
.Select(EntityToCrdExtensions.CreateCrd);
21588
}
21689

21790
private static IEnumerable<Type> GetTypesWithAttribute<TAttribute>(Assembly assembly)
21891
where TAttribute : Attribute =>
21992
assembly.GetTypes().Where(type => type.GetCustomAttributes<TAttribute>().Any());
220-
221-
private static bool IsSimpleType(Type type) =>
222-
type.IsPrimitive ||
223-
new[]
224-
{
225-
typeof(string),
226-
typeof(decimal),
227-
typeof(DateTime),
228-
typeof(DateTimeOffset),
229-
typeof(TimeSpan),
230-
typeof(Guid)
231-
}.Contains(type) ||
232-
type.IsEnum ||
233-
Convert.GetTypeCode(type) != TypeCode.Object ||
234-
(type.IsGenericType &&
235-
type.GetGenericTypeDefinition() == typeof(Nullable<>) &&
236-
IsSimpleType(type.GetGenericArguments()[0]));
237-
238-
private static string CamelCase(string str) => $"{str.Substring(0, 1).ToLower()}{str.Substring(1)}";
23993
}
24094
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace KubeOps.Operator.Entities.Annotations
4+
{
5+
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)]
6+
public class DescriptionAttribute : Attribute
7+
{
8+
public DescriptionAttribute(string description)
9+
{
10+
Description = description;
11+
}
12+
13+
public string Description { get; }
14+
}
15+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
3+
namespace KubeOps.Operator.Entities.Annotations
4+
{
5+
[AttributeUsage(AttributeTargets.Property)]
6+
public class ExternalDocsAttribute : Attribute
7+
{
8+
public ExternalDocsAttribute(string url, string? description = null)
9+
{
10+
Description = description;
11+
Url = url;
12+
}
13+
14+
public string? Description { get; }
15+
16+
public string Url { get; }
17+
}
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
3+
namespace KubeOps.Operator.Entities.Annotations
4+
{
5+
[AttributeUsage(AttributeTargets.Property)]
6+
public class ItemsAttribute : Attribute
7+
{
8+
public long MinItems { get; set; } = -1;
9+
10+
public long MaxItems { get; set; } = -1;
11+
12+
public bool UniqueItems { get; set; }
13+
}
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
3+
namespace KubeOps.Operator.Entities.Annotations
4+
{
5+
[AttributeUsage(AttributeTargets.Property)]
6+
public class LengthAttribute : Attribute
7+
{
8+
public int MinLength { get; set; } = -1;
9+
10+
public int MaxLength { get; set; } = -1;
11+
}
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace KubeOps.Operator.Entities.Annotations
4+
{
5+
[AttributeUsage(AttributeTargets.Property)]
6+
public class MultipleOfAttribute : Attribute
7+
{
8+
public MultipleOfAttribute(double value)
9+
{
10+
Value = value;
11+
}
12+
13+
public double Value { get; }
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace KubeOps.Operator.Entities.Annotations
4+
{
5+
[AttributeUsage(AttributeTargets.Property)]
6+
public class PatternAttribute : Attribute
7+
{
8+
public PatternAttribute(string regexPattern)
9+
{
10+
RegexPattern = regexPattern;
11+
}
12+
13+
public string RegexPattern { get; }
14+
}
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
3+
namespace KubeOps.Operator.Entities.Annotations
4+
{
5+
[AttributeUsage(AttributeTargets.Property)]
6+
public class RangeMaximumAttribute : Attribute
7+
{
8+
public double Maximum { get; set; } = -1;
9+
10+
public bool ExclusiveMaximum { get; set; }
11+
}
12+
}

0 commit comments

Comments
 (0)