Skip to content

Commit f1125e9

Browse files
authored
Implementing a more modular API (#1627)
* fix typo * Add modular API * Use x-kubernetes-action instead of operationId to generate method names * fix * Clean code style warnings * Refactor client constructors to use Kubernetes type instead of IKubernetes * Add ClientSet tests for Kubernetes pod operations * Fix order of parameters in Pod API calls for consistency * Enhance documentation for ClientSet and ResourceClient classes * Refactor ClientSet and GroupClient for Kubernetes usage * Refactor Pod API calls in tests to use singular form for consistency * Refactor Pod API calls to use 'Create' and 'Update' methods for consistency
1 parent e6317b8 commit f1125e9

20 files changed

+488
-34
lines changed

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,4 @@
5050
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.7.112" />
5151
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
5252
</ItemGroup>
53-
</Project>
53+
</Project>

examples/clientset/Program.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// See https://aka.ms/new-console-template for more information
2+
using k8s;
3+
using k8s.ClientSets;
4+
using System.Threading.Tasks;
5+
6+
namespace clientset
7+
{
8+
internal class Program
9+
{
10+
private static async Task Main(string[] args)
11+
{
12+
var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
13+
var client = new Kubernetes(config);
14+
15+
ClientSet clientSet = new ClientSet(client);
16+
var list = await clientSet.CoreV1.Pod.ListAsync("default").ConfigureAwait(false);
17+
foreach (var item in list)
18+
{
19+
System.Console.WriteLine(item.Metadata.Name);
20+
}
21+
22+
var pod = await clientSet.CoreV1.Pod.GetAsync("test","default").ConfigureAwait(false);
23+
System.Console.WriteLine(pod?.Metadata?.Name);
24+
}
25+
}
26+
}

examples/clientset/clientset.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
</PropertyGroup>
5+
6+
</Project>

src/KubernetesClient.Aot/KubernetesClient.Aot.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
<Compile Include="..\KubernetesClient\Models\V1Status.cs" />
4747

4848
</ItemGroup>
49+
<ItemGroup>
50+
<Compile Include="..\KubernetesClient\ClientSets\ClientSet.cs" />
51+
<Compile Include="..\KubernetesClient\ClientSets\ResourceClient.cs"/>
52+
</ItemGroup>
4953
<ItemGroup>
5054
<Compile Include="..\KubernetesClient\AbstractKubernetes.cs" />
5155
<Compile Include="..\KubernetesClient\GeneratedApiVersion.cs" />
@@ -107,4 +111,4 @@
107111
<ItemGroup>
108112
<ProjectReference Include="..\LibKubernetesGenerator\generators\LibKubernetesGenerator\LibKubernetesGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
109113
</ItemGroup>
110-
</Project>
114+
</Project>

src/KubernetesClient.Classic/KubernetesClient.Classic.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@
7575
<Compile Include="..\KubernetesClient\Autorest\HttpRequestMessageWrapper.cs" />
7676
<Compile Include="..\KubernetesClient\Autorest\HttpResponseMessageWrapper.cs" />
7777
</ItemGroup>
78-
78+
<ItemGroup>
79+
<Compile Include="..\KubernetesClient\ClientSets\ClientSet.cs" />
80+
<Compile Include="..\KubernetesClient\ClientSets\ResourceClient.cs"/>
81+
</ItemGroup>
7982
<ItemGroup>
8083
<Compile Include="..\KubernetesClient\FileSystem.cs" />
8184
<Compile Include="..\KubernetesClient\IKubernetes.cs" />
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace k8s.ClientSets
2+
{
3+
/// <summary>
4+
/// Represents a base class for clients that interact with Kubernetes resources.
5+
/// Provides shared functionality for derived resource-specific clients.
6+
/// </summary>
7+
public partial class ClientSet
8+
{
9+
private readonly Kubernetes _kubernetes;
10+
11+
public ClientSet(Kubernetes kubernetes)
12+
{
13+
_kubernetes = kubernetes;
14+
}
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace k8s.ClientSets
2+
{
3+
/// <summary>
4+
/// Represents a set of Kubernetes clients for interacting with the Kubernetes API.
5+
/// This class provides access to various client implementations for managing Kubernetes resources.
6+
/// </summary>
7+
public abstract class ResourceClient
8+
{
9+
protected Kubernetes Client { get; }
10+
11+
public ResourceClient(Kubernetes kubernetes)
12+
{
13+
Client = kubernetes;
14+
}
15+
}
16+
}

src/KubernetesClient/Kubernetes.WebSocket.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,19 @@ public partial class Kubernetes
1818
/// <inheritdoc/>
1919
public Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default",
2020
string command = null, string container = null, bool stderr = true, bool stdin = true, bool stdout = true,
21-
bool tty = true, string webSocketSubProtol = null, Dictionary<string, List<string>> customHeaders = null,
21+
bool tty = true, string webSocketSubProtocol = null, Dictionary<string, List<string>> customHeaders = null,
2222
CancellationToken cancellationToken = default)
2323
{
2424
return WebSocketNamespacedPodExecAsync(name, @namespace, new string[] { command }, container, stderr, stdin,
25-
stdout, tty, webSocketSubProtol, customHeaders, cancellationToken);
25+
stdout, tty, webSocketSubProtocol, customHeaders, cancellationToken);
2626
}
2727

2828
/// <inheritdoc/>
2929
public virtual async Task<IStreamDemuxer> MuxedStreamNamespacedPodExecAsync(
3030
string name,
3131
string @namespace = "default", IEnumerable<string> command = null, string container = null,
3232
bool stderr = true, bool stdin = true, bool stdout = true, bool tty = true,
33-
string webSocketSubProtol = WebSocketProtocol.V4BinaryWebsocketProtocol,
33+
string webSocketSubProtocol = WebSocketProtocol.V4BinaryWebsocketProtocol,
3434
Dictionary<string, List<string>> customHeaders = null,
3535
CancellationToken cancellationToken = default)
3636
{
@@ -45,7 +45,7 @@ public virtual async Task<IStreamDemuxer> MuxedStreamNamespacedPodExecAsync(
4545
public virtual Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, string @namespace = "default",
4646
IEnumerable<string> command = null, string container = null, bool stderr = true, bool stdin = true,
4747
bool stdout = true, bool tty = true,
48-
string webSocketSubProtol = WebSocketProtocol.V4BinaryWebsocketProtocol,
48+
string webSocketSubProtocol = WebSocketProtocol.V4BinaryWebsocketProtocol,
4949
Dictionary<string, List<string>> customHeaders = null,
5050
CancellationToken cancellationToken = default)
5151
{
@@ -114,7 +114,7 @@ public virtual Task<WebSocket> WebSocketNamespacedPodExecAsync(string name, stri
114114
uriBuilder.Query =
115115
query.ToString(1, query.Length - 1); // UriBuilder.Query doesn't like leading '?' chars, so trim it
116116

117-
return StreamConnectAsync(uriBuilder.Uri, webSocketSubProtol, customHeaders,
117+
return StreamConnectAsync(uriBuilder.Uri, webSocketSubProtocol, customHeaders,
118118
cancellationToken);
119119
}
120120

@@ -173,7 +173,7 @@ public Task<WebSocket> WebSocketNamespacedPodPortForwardAsync(string name, strin
173173
/// <inheritdoc/>
174174
public Task<WebSocket> WebSocketNamespacedPodAttachAsync(string name, string @namespace,
175175
string container = default, bool stderr = true, bool stdin = false, bool stdout = true,
176-
bool tty = false, string webSocketSubProtol = null, Dictionary<string, List<string>> customHeaders = null,
176+
bool tty = false, string webSocketSubProtocol = null, Dictionary<string, List<string>> customHeaders = null,
177177
CancellationToken cancellationToken = default)
178178
{
179179
if (name == null)
@@ -208,7 +208,7 @@ public Task<WebSocket> WebSocketNamespacedPodAttachAsync(string name, string @na
208208
uriBuilder.Query =
209209
query.ToString(1, query.Length - 1); // UriBuilder.Query doesn't like leading '?' chars, so trim it
210210

211-
return StreamConnectAsync(uriBuilder.Uri, webSocketSubProtol, customHeaders,
211+
return StreamConnectAsync(uriBuilder.Uri, webSocketSubProtocol, customHeaders,
212212
cancellationToken);
213213
}
214214

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using CaseExtensions;
2+
using Microsoft.CodeAnalysis;
3+
using NSwag;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
7+
namespace LibKubernetesGenerator
8+
{
9+
internal class ClientSetGenerator
10+
{
11+
private readonly ScriptObjectFactory _scriptObjectFactory;
12+
13+
public ClientSetGenerator(ScriptObjectFactory scriptObjectFactory)
14+
{
15+
_scriptObjectFactory = scriptObjectFactory;
16+
}
17+
18+
public void Generate(OpenApiDocument swagger, IncrementalGeneratorPostInitializationContext context)
19+
{
20+
var data = swagger.Operations
21+
.Where(o => o.Method != OpenApiOperationMethod.Options)
22+
.Select(o =>
23+
{
24+
var ps = o.Operation.ActualParameters.OrderBy(p => !p.IsRequired).ToArray();
25+
26+
o.Operation.Parameters.Clear();
27+
28+
var name = new HashSet<string>();
29+
30+
var i = 1;
31+
foreach (var p in ps)
32+
{
33+
if (name.Contains(p.Name))
34+
{
35+
p.Name += i++;
36+
}
37+
38+
o.Operation.Parameters.Add(p);
39+
name.Add(p.Name);
40+
}
41+
42+
return o;
43+
})
44+
.Select(o =>
45+
{
46+
o.Path = o.Path.TrimStart('/');
47+
o.Method = char.ToUpper(o.Method[0]) + o.Method.Substring(1);
48+
return o;
49+
})
50+
.ToArray();
51+
52+
var sc = _scriptObjectFactory.CreateScriptObject();
53+
54+
var groups = new List<string>();
55+
var apiGroups = new Dictionary<string, OpenApiOperationDescription[]>();
56+
57+
foreach (var grouped in data.Where(d => HasKubernetesAction(d.Operation?.ExtensionData))
58+
.GroupBy(d => d.Operation.Tags.First()))
59+
{
60+
var clients = new List<string>();
61+
var name = grouped.Key.ToPascalCase();
62+
groups.Add(name);
63+
var apis = grouped.Select(x =>
64+
{
65+
var groupVersionKindElements = x.Operation?.ExtensionData?["x-kubernetes-group-version-kind"];
66+
var groupVersionKind = (Dictionary<string, object>)groupVersionKindElements;
67+
68+
return new { Kind = groupVersionKind?["kind"] as string, Api = x };
69+
});
70+
71+
foreach (var item in apis.GroupBy(x => x.Kind))
72+
{
73+
var kind = item.Key;
74+
apiGroups[kind] = item.Select(x => x.Api).ToArray();
75+
clients.Add(kind);
76+
}
77+
78+
sc.SetValue("clients", clients, true);
79+
sc.SetValue("name", name, true);
80+
context.RenderToContext("GroupClient.cs.template", sc, $"{name}GroupClient.g.cs");
81+
}
82+
83+
foreach (var apiGroup in apiGroups)
84+
{
85+
var name = apiGroup.Key;
86+
var apis = apiGroup.Value.ToArray();
87+
var group = apis.Select(x => x.Operation.Tags[0]).First();
88+
sc.SetValue("apis", apis, true);
89+
sc.SetValue("name", name, true);
90+
sc.SetValue("group", group.ToPascalCase(), true);
91+
context.RenderToContext("Client.cs.template", sc, $"{name}Client.g.cs");
92+
}
93+
94+
sc = _scriptObjectFactory.CreateScriptObject();
95+
sc.SetValue("groups", groups, true);
96+
97+
context.RenderToContext("ClientSet.cs.template", sc, $"ClientSet.g.cs");
98+
}
99+
100+
private bool HasKubernetesAction(IDictionary<string, object> extensionData) =>
101+
extensionData?.ContainsKey("x-kubernetes-action") ?? false;
102+
}
103+
}

src/LibKubernetesGenerator/GeneralNameHelper.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public GeneralNameHelper(ClassNameHelper classNameHelper)
2121
public void RegisterHelper(ScriptObject scriptObject)
2222
{
2323
scriptObject.Import(nameof(GetInterfaceName), new Func<JsonSchema, string>(GetInterfaceName));
24-
scriptObject.Import(nameof(GetMethodName), new Func<OpenApiOperation, string, string>(GetMethodName));
24+
scriptObject.Import(nameof(GetOperationId), new Func<OpenApiOperation, string, string>(GetOperationId));
25+
scriptObject.Import(nameof(GetActionName), new Func<OpenApiOperation, string, string, string>(GetActionName));
2526
scriptObject.Import(nameof(GetDotNetName), new Func<string, string, string>(GetDotNetName));
2627
scriptObject.Import(nameof(GetDotNetNameOpenApiParameter), new Func<OpenApiParameter, string, string>(GetDotNetNameOpenApiParameter));
2728
}
@@ -138,7 +139,7 @@ public string GetDotNetName(string jsonName, string style = "parameter")
138139
return jsonName.ToCamelCase();
139140
}
140141

141-
public static string GetMethodName(OpenApiOperation watchOperation, string suffix)
142+
public static string GetOperationId(OpenApiOperation watchOperation, string suffix)
142143
{
143144
var tag = watchOperation.Tags[0];
144145
tag = tag.Replace("_", string.Empty);
@@ -162,5 +163,28 @@ public static string GetMethodName(OpenApiOperation watchOperation, string suffi
162163

163164
return methodName;
164165
}
166+
167+
public static string GetActionName(OpenApiOperation apiOperation, string resource, string suffix)
168+
{
169+
var operationId = apiOperation.OperationId.ToPascalCase();
170+
var replacements = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
171+
{
172+
{ "Replace", "Update" },
173+
{ "Read", "Get" },
174+
};
175+
176+
foreach (var replacement in replacements)
177+
{
178+
operationId = Regex.Replace(operationId, replacement.Key, replacement.Value, RegexOptions.IgnoreCase);
179+
}
180+
181+
var resources = new[] { resource, "ForAllNamespaces", "Namespaced" };
182+
var pattern = string.Join("|", Array.ConvertAll(resources, Regex.Escape));
183+
var actionName = pattern.Length > 0
184+
? Regex.Replace(operationId, pattern, string.Empty, RegexOptions.IgnoreCase)
185+
: operationId;
186+
187+
return $"{actionName}{suffix}";
188+
}
165189
}
166190
}

src/LibKubernetesGenerator/KubernetesClientSourceGenerator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ private static IContainer BuildContainer(OpenApiDocument swagger)
6161
builder.RegisterType<ModelExtGenerator>();
6262
builder.RegisterType<ModelGenerator>();
6363
builder.RegisterType<ApiGenerator>();
64+
builder.RegisterType<ClientSetGenerator>();
6465
builder.RegisterType<VersionConverterStubGenerator>();
6566
builder.RegisterType<VersionGenerator>();
6667

@@ -80,6 +81,7 @@ public void Initialize(IncrementalGeneratorInitializationContext generatorContex
8081
container.Resolve<ModelExtGenerator>().Generate(swagger, ctx);
8182
container.Resolve<VersionConverterStubGenerator>().Generate(swagger, ctx);
8283
container.Resolve<ApiGenerator>().Generate(swagger, ctx);
84+
container.Resolve<ClientSetGenerator>().Generate(swagger, ctx);
8385
});
8486
#endif
8587

src/LibKubernetesGenerator/LibKubernetesGenerator.target

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19+
1920
<!-- Scriban No Dependency -->
2021
<PackageReference Include="Scriban" IncludeAssets="Build" />
2122

0 commit comments

Comments
 (0)