Skip to content

Commit c48fc0d

Browse files
authored
Add interfaces for generated Kubernetes objects (#378)
* Add interfaces for generated Kubernetes objects that can allow working with them without using concrete types. This work is needed for future shared informers / controllers components being developed * Add metadata for plural names. This opens up a path for many generic operations as plural name is needed to construct path
1 parent 40026e4 commit c48fc0d

File tree

11 files changed

+693
-193
lines changed

11 files changed

+693
-193
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ bin/
99
*.user
1010
*.userosscache
1111
*.sln.docstates
12+
13+
# JetBrains Rider
14+
.idea/
15+
*.sln.iml

gen/KubernetesWatchGenerator/ModelExtensions.cs.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
namespace k8s.Models
22
{
33
{{#.}}
4-
public partial class {{GetClassName . }} : IKubernetesObject
4+
[KubernetesEntity(Group="{{GetGroup . }}", Kind="{{GetKind . }}", ApiVersion="{{GetApiVersion . }}", PluralName={{GetPlural .}})]
5+
public partial class {{GetClassName . }} : {{GetInterfaceName . }}
56
{
67
public const string KubeApiVersion = "{{GetApiVersion . }}";
78
public const string KubeKind = "{{GetKind . }}";

gen/KubernetesWatchGenerator/Program.cs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Collections.ObjectModel;
7+
using System.Diagnostics;
78
using System.IO;
89
using System.Linq;
910
using System.Net.Http;
@@ -13,7 +14,11 @@ namespace KubernetesWatchGenerator
1314
{
1415
class Program
1516
{
17+
private static HashSet<string> _classesWithValidation;
1618
static readonly Dictionary<string, string> ClassNameMap = new Dictionary<string, string>();
19+
private static Dictionary<JsonSchema4, string> _schemaToNameMap;
20+
private static HashSet<string> _schemaDefinitionsInMultipleGroups;
21+
private static Dictionary<string, string> _classNameToPluralMap;
1722

1823
static async Task Main(string[] args)
1924
{
@@ -45,17 +50,47 @@ static async Task Main(string[] args)
4550

4651
// gen project removed all watch operations, so here we switch back to unprocessed version
4752
swagger = await SwaggerDocument.FromFileAsync(Path.Combine(args[1], "swagger.json.unprocessed"));
53+
_schemaToNameMap = swagger.Definitions.ToDictionary(x => x.Value, x => x.Key);
54+
_schemaDefinitionsInMultipleGroups = _schemaToNameMap.Values.Select(x =>
55+
{
56+
var parts = x.Split(".");
57+
return new {FullName = x, Name = parts[parts.Length - 1], Version = parts[parts.Length - 2], Group = parts[parts.Length - 3]};
58+
})
59+
.GroupBy(x => new {x.Name, x.Version})
60+
.Where(x => x.Count() > 1)
61+
.SelectMany(x => x)
62+
.Select(x => x.FullName)
63+
.ToHashSet();
64+
65+
_classNameToPluralMap = swagger.Operations
66+
.Where(x => x.Operation.OperationId.StartsWith("list"))
67+
.Select(x => { return new {PluralName = x.Path.Split("/").Last(), ClassName = GetClassNameForSchemaDefinition(x.Operation.Responses["200"].ActualResponseSchema)}; })
68+
.Distinct()
69+
.ToDictionary(x => x.ClassName, x => x.PluralName);
70+
71+
// dictionary only contains "list" plural maps. assign the same plural names to entities those lists support
72+
_classNameToPluralMap = _classNameToPluralMap
73+
.Where(x => x.Key.EndsWith("List"))
74+
.Select(x =>
75+
new {ClassName = x.Key.Remove(x.Key.Length - 4), PluralName = x.Value})
76+
.ToDictionary(x => x.ClassName, x => x.PluralName)
77+
.Union(_classNameToPluralMap)
78+
.ToDictionary(x => x.Key, x => x.Value);
79+
80+
4881

4982
// Register helpers used in the templating.
5083
Helpers.Register(nameof(ToXmlDoc), ToXmlDoc);
5184
Helpers.Register(nameof(GetClassName), GetClassName);
85+
Helpers.Register(nameof(GetInterfaceName), GetInterfaceName);
5286
Helpers.Register(nameof(GetMethodName), GetMethodName);
5387
Helpers.Register(nameof(GetDotNetName), GetDotNetName);
5488
Helpers.Register(nameof(GetDotNetType), GetDotNetType);
5589
Helpers.Register(nameof(GetPathExpression), GetPathExpression);
5690
Helpers.Register(nameof(GetGroup), GetGroup);
5791
Helpers.Register(nameof(GetApiVersion), GetApiVersion);
5892
Helpers.Register(nameof(GetKind), GetKind);
93+
Helpers.Register(nameof(GetPlural), GetPlural);
5994

6095
// Generate the Watcher operations
6196
// We skip operations where the name of the class in the C# client could not be determined correctly.
@@ -85,6 +120,13 @@ static async Task Main(string[] args)
85120
&& d.ExtensionData.ContainsKey("x-kubernetes-group-version-kind")
86121
&& !skippedTypes.Contains(GetClassName(d)));
87122

123+
var modelsDir = Path.Combine(outputDirectory, "Models");
124+
_classesWithValidation = Directory.EnumerateFiles(modelsDir)
125+
.Select(x => new {Class = Path.GetFileNameWithoutExtension(x), Content = File.ReadAllText(x)})
126+
.Where(x => x.Content.Contains("public virtual void Validate()"))
127+
.Select(x => x.Class)
128+
.ToHashSet();
129+
88130
Render.FileToFile("ModelExtensions.cs.template", definitions, Path.Combine(outputDirectory, "ModelExtensions.cs"));
89131
}
90132

@@ -148,6 +190,66 @@ private static string GetClassName(JsonSchema4 definition)
148190

149191
return GetClassName(groupVersionKind);
150192
}
193+
private static void GetInterfaceName(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
194+
{
195+
196+
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is JsonSchema4)
197+
{
198+
context.Write(GetInterfaceName(arguments[0] as JsonSchema4));
199+
}
200+
201+
}
202+
203+
static string GetClassNameForSchemaDefinition(JsonSchema4 definition)
204+
{
205+
if (definition.ExtensionData != null && definition.ExtensionData.ContainsKey("x-kubernetes-group-version-kind"))
206+
return GetClassName(definition);
207+
208+
var schemaName = _schemaToNameMap[definition];
209+
210+
var parts = schemaName.Split(".");
211+
var group = parts[parts.Length - 3];
212+
var version = parts[parts.Length - 2];
213+
var entityName = parts[parts.Length - 1];
214+
if (!_schemaDefinitionsInMultipleGroups.Contains(schemaName))
215+
group = null;
216+
var className = ToPascalCase($"{group}{version}{entityName}");
217+
return className;
218+
219+
}
220+
static string GetInterfaceName(JsonSchema4 definition)
221+
{
222+
var groupVersionKindElements = (object[])definition.ExtensionData["x-kubernetes-group-version-kind"];
223+
var groupVersionKind = (Dictionary<string, object>)groupVersionKindElements[0];
224+
225+
var group = groupVersionKind["group"] as string;
226+
var version = groupVersionKind["version"] as string;
227+
var kind = groupVersionKind["kind"] as string;
228+
var className = GetClassName(definition);
229+
var interfaces = new List<string>();
230+
interfaces.Add("IKubernetesObject");
231+
if (definition.Properties.TryGetValue("metadata", out var metadataProperty))
232+
{
233+
interfaces.Add($"IMetadata<{GetClassNameForSchemaDefinition(metadataProperty.Reference)}>");
234+
}
235+
236+
if (definition.Properties.TryGetValue("items", out var itemsProperty))
237+
{
238+
var schema = itemsProperty.Type == JsonObjectType.Object ? itemsProperty.Reference : itemsProperty.Item.Reference;
239+
interfaces.Add($"IItems<{GetClassNameForSchemaDefinition(schema)}>");
240+
}
241+
242+
if (definition.Properties.TryGetValue("spec", out var specProperty))
243+
{
244+
interfaces.Add($"ISpec<{GetClassNameForSchemaDefinition(specProperty.Reference)}>");
245+
}
246+
247+
if(_classesWithValidation.Contains(className))
248+
interfaces.Add("IValidate");
249+
var result = string.Join(", ", interfaces);
250+
return result;
251+
}
252+
151253

152254
static void GetKind(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
153255
{
@@ -165,6 +267,24 @@ private static string GetKind(JsonSchema4 definition)
165267
return groupVersionKind["kind"] as string;
166268
}
167269

270+
static void GetPlural(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
271+
{
272+
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is JsonSchema4)
273+
{
274+
var plural = GetPlural(arguments[0] as JsonSchema4);
275+
if(plural != null)
276+
context.Write($"\"{plural}\"");
277+
else
278+
context.Write("null");
279+
}
280+
}
281+
282+
private static string GetPlural(JsonSchema4 definition)
283+
{
284+
var className = GetClassNameForSchemaDefinition(definition);
285+
return _classNameToPluralMap.GetValueOrDefault(className, null);
286+
}
287+
168288
static void GetGroup(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
169289
{
170290
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is JsonSchema4)

src/KubernetesClient/IItems.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Collections.Generic;
2+
3+
namespace k8s
4+
{
5+
/// <summary>
6+
/// Kubernetes object that exposes list of objects
7+
/// </summary>
8+
/// <typeparam name="T"></typeparam>
9+
public interface IItems<T>
10+
{
11+
/// <summary>
12+
/// Gets or sets list of objects. More info:
13+
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md
14+
/// </summary>
15+
IList<T> Items { get; set; }
16+
}
17+
}

src/KubernetesClient/IMetadata.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using k8s.Models;
2+
3+
namespace k8s
4+
{
5+
/// <summary>
6+
/// Kubernetes object that exposes metadata
7+
/// </summary>
8+
/// <typeparam name="T">Type of metadata exposed. Usually this will be either
9+
/// <see cref="V1ListMeta"/> for lists or <see cref="V1ObjectMeta"/> for objects</typeparam>
10+
public interface IMetadata<T>
11+
{
12+
/// <summary>
13+
/// Gets or sets standard object's metadata. More info:
14+
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
15+
/// </summary>
16+
T Metadata { get; set; }
17+
}
18+
}

src/KubernetesClient/ISpec.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace k8s
2+
{
3+
/// <summary>
4+
/// Represents a Kubernetes object that has a spec
5+
/// </summary>
6+
/// <typeparam name="T"></typeparam>
7+
public interface ISpec<T>
8+
{
9+
/// <summary>
10+
/// Gets or sets specification of the desired behavior of the entity. More
11+
/// info:
12+
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
13+
/// </summary>
14+
/// </summary>
15+
T Spec { get; set; }
16+
}
17+
}

src/KubernetesClient/IStatus.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace k8s
2+
{
3+
/// <summary>
4+
/// Kubernetes object that exposes status
5+
/// </summary>
6+
/// <typeparam name="T">The type of status object</typeparam>
7+
public interface IStatus<T>
8+
{
9+
/// <summary>
10+
/// Gets or sets most recently observed status of the object. This data
11+
/// may not be up to date. Populated by the system. Read-only. More
12+
/// info:
13+
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
14+
/// </summary>
15+
T Status { get; set; }
16+
}
17+
}

src/KubernetesClient/IValidate.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace k8s
2+
{
3+
/// <summary>
4+
/// Object that allows self validation
5+
/// </summary>
6+
public interface IValidate
7+
{
8+
/// <summary>
9+
/// Validate the object.
10+
/// </summary>
11+
void Validate();
12+
13+
}
14+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
3+
namespace k8s.Models
4+
{
5+
/// <summary>
6+
/// Describes object type in Kubernetes
7+
/// </summary>
8+
public class KubernetesEntityAttribute : Attribute
9+
{
10+
/// <summary>
11+
/// The Kubernetes named schema this object is based on
12+
/// </summary>
13+
public string Kind { get; set; }
14+
/// <summary>
15+
/// The Group this Kubernetes type belongs to
16+
/// </summary>
17+
public string Group { get; set; }
18+
/// <summary>
19+
/// The API Version this Kubernetes type belongs to
20+
/// </summary>
21+
public string ApiVersion { get; set; }
22+
/// <summary>
23+
/// The plural name of the entity
24+
/// </summary>
25+
public string PluralName { get; set; }
26+
}
27+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using k8s.Models;
4+
using Microsoft.Rest;
5+
using Newtonsoft.Json;
6+
7+
namespace k8s.Models
8+
{
9+
10+
public class KubernetesList<T> : IMetadata<V1ListMeta>, IItems<T> where T : IKubernetesObject
11+
{
12+
13+
public KubernetesList(IList<T> items, string apiVersion = default(string), string kind = default(string), V1ListMeta metadata = default(V1ListMeta))
14+
{
15+
ApiVersion = apiVersion;
16+
Items = items;
17+
Kind = kind;
18+
Metadata = metadata;
19+
}
20+
21+
22+
/// <summary>
23+
/// Gets or sets aPIVersion defines the versioned schema of this
24+
/// representation of an object. Servers should convert recognized
25+
/// schemas to the latest internal value, and may reject unrecognized
26+
/// values. More info:
27+
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
28+
/// </summary>
29+
[JsonProperty(PropertyName = "apiVersion")]
30+
public string ApiVersion { get; set; }
31+
32+
[JsonProperty(PropertyName = "items")]
33+
public IList<T> Items { get; set; }
34+
35+
/// <summary>
36+
/// Gets or sets kind is a string value representing the REST resource
37+
/// this object represents. Servers may infer this from the endpoint
38+
/// the client submits requests to. Cannot be updated. In CamelCase.
39+
/// More info:
40+
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
41+
/// </summary>
42+
[JsonProperty(PropertyName = "kind")]
43+
public string Kind { get; set; }
44+
45+
/// <summary>
46+
/// Gets or sets standard object's metadata.
47+
/// </summary>
48+
[JsonProperty(PropertyName = "metadata")]
49+
public V1ListMeta Metadata { get; set; }
50+
51+
/// <summary>
52+
/// Validate the object.
53+
/// </summary>
54+
/// <exception cref="ValidationException">
55+
/// Thrown if validation fails
56+
/// </exception>
57+
public void Validate()
58+
{
59+
if (Items == null)
60+
{
61+
throw new ValidationException(ValidationRules.CannotBeNull, "Items");
62+
}
63+
if (Items != null)
64+
{
65+
foreach (var element in Items.OfType<IValidate>())
66+
{
67+
element.Validate();
68+
}
69+
}
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)