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
This commit is contained in:
Andrew Stakhov
2020-03-23 00:22:45 -04:00
committed by GitHub
parent 40026e40a5
commit c48fc0dc56
11 changed files with 693 additions and 193 deletions

4
.gitignore vendored
View File

@@ -9,3 +9,7 @@ bin/
*.user
*.userosscache
*.sln.docstates
# JetBrains Rider
.idea/
*.sln.iml

View File

@@ -1,7 +1,8 @@
namespace k8s.Models
{
{{#.}}
public partial class {{GetClassName . }} : IKubernetesObject
[KubernetesEntity(Group="{{GetGroup . }}", Kind="{{GetKind . }}", ApiVersion="{{GetApiVersion . }}", PluralName={{GetPlural .}})]
public partial class {{GetClassName . }} : {{GetInterfaceName . }}
{
public const string KubeApiVersion = "{{GetApiVersion . }}";
public const string KubeKind = "{{GetKind . }}";

View File

@@ -4,6 +4,7 @@ using Nustache.Core;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
@@ -13,7 +14,11 @@ namespace KubernetesWatchGenerator
{
class Program
{
private static HashSet<string> _classesWithValidation;
static readonly Dictionary<string, string> ClassNameMap = new Dictionary<string, string>();
private static Dictionary<JsonSchema4, string> _schemaToNameMap;
private static HashSet<string> _schemaDefinitionsInMultipleGroups;
private static Dictionary<string, string> _classNameToPluralMap;
static async Task Main(string[] args)
{
@@ -45,10 +50,39 @@ namespace KubernetesWatchGenerator
// gen project removed all watch operations, so here we switch back to unprocessed version
swagger = await SwaggerDocument.FromFileAsync(Path.Combine(args[1], "swagger.json.unprocessed"));
_schemaToNameMap = swagger.Definitions.ToDictionary(x => x.Value, x => x.Key);
_schemaDefinitionsInMultipleGroups = _schemaToNameMap.Values.Select(x =>
{
var parts = x.Split(".");
return new {FullName = x, Name = parts[parts.Length - 1], Version = parts[parts.Length - 2], Group = parts[parts.Length - 3]};
})
.GroupBy(x => new {x.Name, x.Version})
.Where(x => x.Count() > 1)
.SelectMany(x => x)
.Select(x => x.FullName)
.ToHashSet();
_classNameToPluralMap = swagger.Operations
.Where(x => x.Operation.OperationId.StartsWith("list"))
.Select(x => { return new {PluralName = x.Path.Split("/").Last(), ClassName = GetClassNameForSchemaDefinition(x.Operation.Responses["200"].ActualResponseSchema)}; })
.Distinct()
.ToDictionary(x => x.ClassName, x => x.PluralName);
// dictionary only contains "list" plural maps. assign the same plural names to entities those lists support
_classNameToPluralMap = _classNameToPluralMap
.Where(x => x.Key.EndsWith("List"))
.Select(x =>
new {ClassName = x.Key.Remove(x.Key.Length - 4), PluralName = x.Value})
.ToDictionary(x => x.ClassName, x => x.PluralName)
.Union(_classNameToPluralMap)
.ToDictionary(x => x.Key, x => x.Value);
// Register helpers used in the templating.
Helpers.Register(nameof(ToXmlDoc), ToXmlDoc);
Helpers.Register(nameof(GetClassName), GetClassName);
Helpers.Register(nameof(GetInterfaceName), GetInterfaceName);
Helpers.Register(nameof(GetMethodName), GetMethodName);
Helpers.Register(nameof(GetDotNetName), GetDotNetName);
Helpers.Register(nameof(GetDotNetType), GetDotNetType);
@@ -56,6 +90,7 @@ namespace KubernetesWatchGenerator
Helpers.Register(nameof(GetGroup), GetGroup);
Helpers.Register(nameof(GetApiVersion), GetApiVersion);
Helpers.Register(nameof(GetKind), GetKind);
Helpers.Register(nameof(GetPlural), GetPlural);
// Generate the Watcher operations
// We skip operations where the name of the class in the C# client could not be determined correctly.
@@ -85,6 +120,13 @@ namespace KubernetesWatchGenerator
&& d.ExtensionData.ContainsKey("x-kubernetes-group-version-kind")
&& !skippedTypes.Contains(GetClassName(d)));
var modelsDir = Path.Combine(outputDirectory, "Models");
_classesWithValidation = Directory.EnumerateFiles(modelsDir)
.Select(x => new {Class = Path.GetFileNameWithoutExtension(x), Content = File.ReadAllText(x)})
.Where(x => x.Content.Contains("public virtual void Validate()"))
.Select(x => x.Class)
.ToHashSet();
Render.FileToFile("ModelExtensions.cs.template", definitions, Path.Combine(outputDirectory, "ModelExtensions.cs"));
}
@@ -148,6 +190,66 @@ namespace KubernetesWatchGenerator
return GetClassName(groupVersionKind);
}
private static void GetInterfaceName(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
{
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is JsonSchema4)
{
context.Write(GetInterfaceName(arguments[0] as JsonSchema4));
}
}
static string GetClassNameForSchemaDefinition(JsonSchema4 definition)
{
if (definition.ExtensionData != null && definition.ExtensionData.ContainsKey("x-kubernetes-group-version-kind"))
return GetClassName(definition);
var schemaName = _schemaToNameMap[definition];
var parts = schemaName.Split(".");
var group = parts[parts.Length - 3];
var version = parts[parts.Length - 2];
var entityName = parts[parts.Length - 1];
if (!_schemaDefinitionsInMultipleGroups.Contains(schemaName))
group = null;
var className = ToPascalCase($"{group}{version}{entityName}");
return className;
}
static string GetInterfaceName(JsonSchema4 definition)
{
var groupVersionKindElements = (object[])definition.ExtensionData["x-kubernetes-group-version-kind"];
var groupVersionKind = (Dictionary<string, object>)groupVersionKindElements[0];
var group = groupVersionKind["group"] as string;
var version = groupVersionKind["version"] as string;
var kind = groupVersionKind["kind"] as string;
var className = GetClassName(definition);
var interfaces = new List<string>();
interfaces.Add("IKubernetesObject");
if (definition.Properties.TryGetValue("metadata", out var metadataProperty))
{
interfaces.Add($"IMetadata<{GetClassNameForSchemaDefinition(metadataProperty.Reference)}>");
}
if (definition.Properties.TryGetValue("items", out var itemsProperty))
{
var schema = itemsProperty.Type == JsonObjectType.Object ? itemsProperty.Reference : itemsProperty.Item.Reference;
interfaces.Add($"IItems<{GetClassNameForSchemaDefinition(schema)}>");
}
if (definition.Properties.TryGetValue("spec", out var specProperty))
{
interfaces.Add($"ISpec<{GetClassNameForSchemaDefinition(specProperty.Reference)}>");
}
if(_classesWithValidation.Contains(className))
interfaces.Add("IValidate");
var result = string.Join(", ", interfaces);
return result;
}
static void GetKind(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
{
@@ -165,6 +267,24 @@ namespace KubernetesWatchGenerator
return groupVersionKind["kind"] as string;
}
static void GetPlural(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
{
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is JsonSchema4)
{
var plural = GetPlural(arguments[0] as JsonSchema4);
if(plural != null)
context.Write($"\"{plural}\"");
else
context.Write("null");
}
}
private static string GetPlural(JsonSchema4 definition)
{
var className = GetClassNameForSchemaDefinition(definition);
return _classNameToPluralMap.GetValueOrDefault(className, null);
}
static void GetGroup(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
{
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is JsonSchema4)

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace k8s
{
/// <summary>
/// Kubernetes object that exposes list of objects
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IItems<T>
{
/// <summary>
/// Gets or sets list of objects. More info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md
/// </summary>
IList<T> Items { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using k8s.Models;
namespace k8s
{
/// <summary>
/// Kubernetes object that exposes metadata
/// </summary>
/// <typeparam name="T">Type of metadata exposed. Usually this will be either
/// <see cref="V1ListMeta"/> for lists or <see cref="V1ObjectMeta"/> for objects</typeparam>
public interface IMetadata<T>
{
/// <summary>
/// Gets or sets standard object's metadata. More info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
/// </summary>
T Metadata { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
namespace k8s
{
/// <summary>
/// Represents a Kubernetes object that has a spec
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ISpec<T>
{
/// <summary>
/// Gets or sets specification of the desired behavior of the entity. More
/// info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
/// </summary>
/// </summary>
T Spec { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
namespace k8s
{
/// <summary>
/// Kubernetes object that exposes status
/// </summary>
/// <typeparam name="T">The type of status object</typeparam>
public interface IStatus<T>
{
/// <summary>
/// Gets or sets most recently observed status of the object. This data
/// may not be up to date. Populated by the system. Read-only. More
/// info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
/// </summary>
T Status { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
namespace k8s
{
/// <summary>
/// Object that allows self validation
/// </summary>
public interface IValidate
{
/// <summary>
/// Validate the object.
/// </summary>
void Validate();
}
}

View File

@@ -0,0 +1,27 @@
using System;
namespace k8s.Models
{
/// <summary>
/// Describes object type in Kubernetes
/// </summary>
public class KubernetesEntityAttribute : Attribute
{
/// <summary>
/// The Kubernetes named schema this object is based on
/// </summary>
public string Kind { get; set; }
/// <summary>
/// The Group this Kubernetes type belongs to
/// </summary>
public string Group { get; set; }
/// <summary>
/// The API Version this Kubernetes type belongs to
/// </summary>
public string ApiVersion { get; set; }
/// <summary>
/// The plural name of the entity
/// </summary>
public string PluralName { get; set; }
}
}

View File

@@ -0,0 +1,72 @@
using System.Collections.Generic;
using System.Linq;
using k8s.Models;
using Microsoft.Rest;
using Newtonsoft.Json;
namespace k8s.Models
{
public class KubernetesList<T> : IMetadata<V1ListMeta>, IItems<T> where T : IKubernetesObject
{
public KubernetesList(IList<T> items, string apiVersion = default(string), string kind = default(string), V1ListMeta metadata = default(V1ListMeta))
{
ApiVersion = apiVersion;
Items = items;
Kind = kind;
Metadata = metadata;
}
/// <summary>
/// Gets or sets aPIVersion defines the versioned schema of this
/// representation of an object. Servers should convert recognized
/// schemas to the latest internal value, and may reject unrecognized
/// values. More info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
/// </summary>
[JsonProperty(PropertyName = "apiVersion")]
public string ApiVersion { get; set; }
[JsonProperty(PropertyName = "items")]
public IList<T> Items { get; set; }
/// <summary>
/// Gets or sets kind is a string value representing the REST resource
/// this object represents. Servers may infer this from the endpoint
/// the client submits requests to. Cannot be updated. In CamelCase.
/// More info:
/// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
/// </summary>
[JsonProperty(PropertyName = "kind")]
public string Kind { get; set; }
/// <summary>
/// Gets or sets standard object's metadata.
/// </summary>
[JsonProperty(PropertyName = "metadata")]
public V1ListMeta Metadata { get; set; }
/// <summary>
/// Validate the object.
/// </summary>
/// <exception cref="ValidationException">
/// Thrown if validation fails
/// </exception>
public void Validate()
{
if (Items == null)
{
throw new ValidationException(ValidationRules.CannotBeNull, "Items");
}
if (Items != null)
{
foreach (var element in Items.OfType<IValidate>())
{
element.Validate();
}
}
}
}
}

File diff suppressed because it is too large Load Diff