Skip to content

Commit ebf8661

Browse files
qmfrederikbrendandburns
authored andcommitted
Auto-generate Watcher code (for individual objects) (#148)
* Only keep WatchObjectAsync<T> in the manually-generated code * Add a console application which can generate the various Watch methods * Generate the Watch methods * Use Nustache and templates to generate the watcher code * Re-generate code
1 parent 29a9c22 commit ebf8661

File tree

9 files changed

+6048
-805
lines changed

9 files changed

+6048
-805
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using k8s.Models;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace k8s
8+
{
9+
public partial interface IKubernetes
10+
{
11+
{{#.}}
12+
/// <summary>
13+
/// {{ToXmlDoc operation.description}}
14+
/// </summary>
15+
{{#operation.actualParameters}}
16+
{{#isRequired}}
17+
/// <param name="{{name}}">
18+
/// {{ToXmlDoc description}}
19+
/// </param>
20+
{{/isRequired}}
21+
{{/operation.actualParameters}}
22+
{{#operation.actualParameters}}
23+
{{^isRequired}}
24+
/// <param name="{{name}}">
25+
/// {{ToXmlDoc description}}
26+
/// </param>
27+
{{/isRequired}}
28+
{{/operation.actualParameters}}
29+
/// <param name="customHeaders">
30+
/// The headers that will be added to request.
31+
/// </param>
32+
/// <param name="onEvent">
33+
/// The action to invoke when the server sends a new event.
34+
/// </param>
35+
/// <param name="onError">
36+
/// The action to invoke when an error occurs.
37+
/// </param>
38+
/// <param name="cancellationToken">
39+
/// A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation.
40+
/// </param>
41+
/// <returns>
42+
/// A <see cref="Task"/> which represents the asynchronous operation, and returns a new watcher.
43+
/// </returns>
44+
Task<Watcher<{{ClassName operation}}>> {{MethodName operation}}(
45+
{{#operation.actualParameters}}
46+
{{#isRequired}}
47+
{{GetDotNetType type name isRequired}} {{GetDotNetName name}},
48+
{{/isRequired}}
49+
{{/operation.actualParameters}}
50+
{{#operation.actualParameters}}
51+
{{^isRequired}}
52+
{{GetDotNetType .}} {{GetDotNetName .}} = null,
53+
{{/isRequired}}
54+
{{/operation.actualParameters}}
55+
Dictionary<string, List<string>> customHeaders = null,
56+
Action<WatchEventType, {{ClassName operation}}> onEvent = null,
57+
Action<Exception> onError = null,
58+
CancellationToken cancellationToken = default(CancellationToken));
59+
60+
{{/.}}
61+
}
62+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using k8s.Models;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace k8s
8+
{
9+
public partial class Kubernetes
10+
{
11+
{{#.}}
12+
/// <inheritdoc>
13+
public Task<Watcher<{{ClassName operation}}>> {{MethodName operation}}(
14+
{{#operation.actualParameters}}
15+
{{#isRequired}}
16+
{{GetDotNetType type name isRequired}} {{GetDotNetName name}},
17+
{{/isRequired}}
18+
{{/operation.actualParameters}}
19+
{{#operation.actualParameters}}
20+
{{^isRequired}}
21+
{{GetDotNetType .}} {{GetDotNetName .}} = null,
22+
{{/isRequired}}
23+
{{/operation.actualParameters}}
24+
Dictionary<string, List<string>> customHeaders = null,
25+
Action<WatchEventType, {{ClassName operation}}> onEvent = null,
26+
Action<Exception> onError = null,
27+
CancellationToken cancellationToken = default(CancellationToken))
28+
{
29+
string path = $"{{GetPathExpression .}}";
30+
return WatchObjectAsync<{{ClassName operation}}>(path: path, @continue: @continue, fieldSelector: fieldSelector, includeUninitialized: includeUninitialized, labelSelector: labelSelector, limit: limit, pretty: pretty, timeoutSeconds: timeoutSeconds, resourceVersion: resourceVersion, customHeaders: customHeaders, onEvent: onEvent, onError: onError, cancellationToken: cancellationToken);
31+
}
32+
33+
{{/.}}
34+
}
35+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp2.0</TargetFramework>
6+
</PropertyGroup>
7+
8+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
9+
<LangVersion>latest</LangVersion>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
14+
<PackageReference Include="NSwag.Core" Version="11.17.2" />
15+
<PackageReference Include="Nustache" Version="1.16.0.8" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<None Update="Kubernetes.Watch.cs.template">
24+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
25+
</None>
26+
<None Update="IKubernetes.Watch.cs.template">
27+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
28+
</None>
29+
</ItemGroup>
30+
31+
</Project>
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
using NJsonSchema;
2+
using NSwag;
3+
using Nustache.Core;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.ObjectModel;
7+
using System.IO;
8+
using System.Linq;
9+
using System.Net.Http;
10+
using System.Threading.Tasks;
11+
12+
namespace KubernetesWatchGenerator
13+
{
14+
class Program
15+
{
16+
static async Task Main(string[] args)
17+
{
18+
// Initialize variables - such as the Kubernetes branch for which to generate the API.
19+
string kubernetesBranch = "v1.10.0";
20+
21+
if (Environment.GetEnvironmentVariable("KUBERNETES_BRANCH") != null)
22+
{
23+
kubernetesBranch = Environment.GetEnvironmentVariable("KUBERNETES_BRANCH");
24+
25+
Console.WriteLine($"Using Kubernetes branch {kubernetesBranch}, as set by the KUBERNETES_BRANCH environment variable");
26+
}
27+
28+
const string outputDirectory = "../../../../../src/KubernetesClient/generated/";
29+
30+
var specUrl = $"https://raw.githubusercontent.com/kubernetes/kubernetes/{kubernetesBranch}/api/openapi-spec/swagger.json";
31+
var specPath = $"{kubernetesBranch}-swagger.json";
32+
33+
// Download the Kubernetes spec, and cache it locally. Don't download it if already present in the cache.
34+
if (!File.Exists(specPath))
35+
{
36+
HttpClient client = new HttpClient();
37+
using (var response = await client.GetAsync(specUrl, HttpCompletionOption.ResponseHeadersRead))
38+
using (var stream = await response.Content.ReadAsStreamAsync())
39+
using (var output = File.Open(specPath, FileMode.Create, FileAccess.ReadWrite))
40+
{
41+
await stream.CopyToAsync(output);
42+
}
43+
}
44+
45+
// Read the spec
46+
var swagger = await SwaggerDocument.FromFileAsync(specPath);
47+
48+
// We skip operations where the name of the class in the C# client could not be determined correctly.
49+
// That's usually because there are different version of the same object (e.g. for deployments).
50+
Collection<string> blacklistedOperations = new Collection<string>()
51+
{
52+
"watchAppsV1beta1NamespacedDeployment",
53+
"watchAppsV1beta2NamespacedDeployment",
54+
"watchExtensionsV1beta1NamespacedDeployment",
55+
"watchExtensionsV1beta1NamespacedNetworkPolicy",
56+
"watchPolicyV1beta1PodSecurityPolicy",
57+
"watchExtensionsV1beta1PodSecurityPolicy"
58+
};
59+
60+
var watchOperations = swagger.Operations.Where(
61+
o => o.Path.Contains("/watch/")
62+
&& o.Operation.ActualParameters.Any(p => p.Name == "name")
63+
&& !blacklistedOperations.Contains(o.Operation.OperationId)).ToArray();
64+
65+
// Register helpers used in the templating.
66+
Helpers.Register(nameof(ToXmlDoc), ToXmlDoc);
67+
Helpers.Register(nameof(ClassName), ClassName);
68+
Helpers.Register(nameof(MethodName), MethodName);
69+
Helpers.Register(nameof(GetDotNetName), GetDotNetName);
70+
Helpers.Register(nameof(GetDotNetType), GetDotNetType);
71+
Helpers.Register(nameof(GetPathExpression), GetPathExpression);
72+
73+
// Render.
74+
Render.FileToFile("IKubernetes.Watch.cs.template", watchOperations, $"{outputDirectory}IKubernetes.Watch.cs");
75+
Render.FileToFile("Kubernetes.Watch.cs.template", watchOperations, $"{outputDirectory}Kubernetes.Watch.cs");
76+
}
77+
78+
static void ToXmlDoc(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
79+
{
80+
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is string)
81+
{
82+
bool first = true;
83+
84+
using (StringReader reader = new StringReader(arguments[0] as string))
85+
{
86+
string line = null;
87+
while ((line = reader.ReadLine()) != null)
88+
{
89+
if (!first)
90+
{
91+
context.Write(Environment.NewLine);
92+
context.Write(" /// ");
93+
}
94+
else
95+
{
96+
first = false;
97+
}
98+
context.Write(line);
99+
}
100+
}
101+
}
102+
}
103+
104+
static void MethodName(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
105+
{
106+
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is SwaggerOperation)
107+
{
108+
context.Write(MethodName(arguments[0] as SwaggerOperation));
109+
}
110+
}
111+
112+
static string MethodName(SwaggerOperation watchOperation)
113+
{
114+
var tag = watchOperation.Tags[0];
115+
tag = tag.Replace("_", string.Empty);
116+
117+
var methodName = ToPascalCase(watchOperation.OperationId);
118+
119+
// This tries to remove the version from the method name, e.g. watchCoreV1NamespacedPod => WatchNamespacedPod
120+
methodName = methodName.Replace(tag, string.Empty, StringComparison.OrdinalIgnoreCase);
121+
methodName += "Async";
122+
return methodName;
123+
}
124+
125+
static void ClassName(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
126+
{
127+
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is SwaggerOperation)
128+
{
129+
context.Write(ClassName(arguments[0] as SwaggerOperation));
130+
}
131+
}
132+
133+
static string ClassName(SwaggerOperation watchOperation)
134+
{
135+
var groupVersionKind = (Dictionary<string, object>)watchOperation.ExtensionData["x-kubernetes-group-version-kind"];
136+
var group = (string)groupVersionKind["group"];
137+
var kind = (string)groupVersionKind["kind"];
138+
var version = (string)groupVersionKind["version"];
139+
140+
var className = $"{ToPascalCase(version)}{kind}";
141+
return className;
142+
}
143+
144+
static void GetDotNetType(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
145+
{
146+
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is SwaggerParameter)
147+
{
148+
var parameter = arguments[0] as SwaggerParameter;
149+
context.Write(GetDotNetType(parameter.Type, parameter.Name, parameter.IsRequired));
150+
}
151+
else if(arguments != null && arguments.Count > 2 && arguments[0] != null && arguments[1] != null && arguments[2] != null && arguments[0] is JsonObjectType && arguments[1] is string && arguments[2] is bool)
152+
{
153+
context.Write(GetDotNetType((JsonObjectType)arguments[0], (string)arguments[1], (bool)arguments[2]));
154+
}
155+
else if(arguments != null && arguments.Count > 0 && arguments[0] != null)
156+
{
157+
context.Write($"ERROR: Expected SwaggerParameter but got {arguments[0].GetType().FullName}");
158+
}
159+
else
160+
{
161+
context.Write($"ERROR: Expected a SwaggerParameter argument but got none.");
162+
}
163+
}
164+
165+
private static string GetDotNetType(JsonObjectType jsonType, string name, bool required)
166+
{
167+
if (name == "pretty" && !required)
168+
{
169+
return "bool?";
170+
}
171+
172+
switch (jsonType)
173+
{
174+
case JsonObjectType.Boolean:
175+
if (required)
176+
{
177+
return "bool";
178+
}
179+
else
180+
{
181+
return "bool?";
182+
}
183+
184+
case JsonObjectType.Integer:
185+
if (required)
186+
{
187+
return "int";
188+
}
189+
else
190+
{
191+
return "int?";
192+
}
193+
194+
case JsonObjectType.String:
195+
return "string";
196+
197+
default:
198+
throw new NotSupportedException();
199+
}
200+
}
201+
202+
static void GetDotNetName(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
203+
{
204+
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is SwaggerParameter)
205+
{
206+
var parameter = arguments[0] as SwaggerParameter;
207+
context.Write(GetDotNetName(parameter.Name));
208+
}
209+
else if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is string)
210+
{
211+
var parameter = arguments[0] as SwaggerParameter;
212+
context.Write(GetDotNetName((string)arguments[0]));
213+
}
214+
}
215+
216+
private static string GetDotNetName(string jsonName)
217+
{
218+
if (jsonName == "namespace")
219+
{
220+
return "@namespace";
221+
}
222+
else if (jsonName == "continue")
223+
{
224+
return "@continue";
225+
}
226+
227+
return jsonName;
228+
}
229+
230+
static void GetPathExpression(RenderContext context, IList<object> arguments, IDictionary<string, object> options, RenderBlock fn, RenderBlock inverse)
231+
{
232+
if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is SwaggerOperationDescription)
233+
{
234+
var operation = arguments[0] as SwaggerOperationDescription;
235+
context.Write(GetPathExpression(operation));
236+
}
237+
}
238+
239+
private static string GetPathExpression(SwaggerOperationDescription operation)
240+
{
241+
string pathExpression = operation.Path;
242+
pathExpression = pathExpression.Replace("{namespace}", "{@namespace}");
243+
return pathExpression;
244+
}
245+
246+
private static string ToPascalCase(string name)
247+
{
248+
return char.ToUpper(name[0]) + name.Substring(1);
249+
}
250+
}
251+
}

0 commit comments

Comments
 (0)