Skip to content

Commit 6a0cdcb

Browse files
committed
Split out KubeClient.Http functionality into a separate package
(and move common functionality to KubeClient.Core) #166
1 parent 6413a39 commit 6a0cdcb

File tree

105 files changed

+448
-32
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+448
-32
lines changed

Diff for: KubeClient.sln

+30
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeClient.Extensions.Custo
5757
EndProject
5858
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeClient.Extensions.CustomResources.Tests", "test\KubeClient.Extensions.CustomResources.Tests\KubeClient.Extensions.CustomResources.Tests.csproj", "{6F6CD966-35A6-4A56-8D4C-D87EEE383374}"
5959
EndProject
60+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeClient.Http", "src\KubeClient.Http\KubeClient.Http.csproj", "{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}"
61+
EndProject
62+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeClient.Core", "src\KubeClient.Core\KubeClient.Core.csproj", "{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}"
63+
EndProject
6064
Global
6165
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6266
Debug|Any CPU = Debug|Any CPU
@@ -343,6 +347,30 @@ Global
343347
{6F6CD966-35A6-4A56-8D4C-D87EEE383374}.Release|x64.Build.0 = Release|Any CPU
344348
{6F6CD966-35A6-4A56-8D4C-D87EEE383374}.Release|x86.ActiveCfg = Release|Any CPU
345349
{6F6CD966-35A6-4A56-8D4C-D87EEE383374}.Release|x86.Build.0 = Release|Any CPU
350+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
351+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
352+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Debug|x64.ActiveCfg = Debug|Any CPU
353+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Debug|x64.Build.0 = Debug|Any CPU
354+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Debug|x86.ActiveCfg = Debug|Any CPU
355+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Debug|x86.Build.0 = Debug|Any CPU
356+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
357+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Release|Any CPU.Build.0 = Release|Any CPU
358+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Release|x64.ActiveCfg = Release|Any CPU
359+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Release|x64.Build.0 = Release|Any CPU
360+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Release|x86.ActiveCfg = Release|Any CPU
361+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7}.Release|x86.Build.0 = Release|Any CPU
362+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
363+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
364+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Debug|x64.ActiveCfg = Debug|Any CPU
365+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Debug|x64.Build.0 = Debug|Any CPU
366+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Debug|x86.ActiveCfg = Debug|Any CPU
367+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Debug|x86.Build.0 = Debug|Any CPU
368+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
369+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Release|Any CPU.Build.0 = Release|Any CPU
370+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Release|x64.ActiveCfg = Release|Any CPU
371+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Release|x64.Build.0 = Release|Any CPU
372+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Release|x86.ActiveCfg = Release|Any CPU
373+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6}.Release|x86.Build.0 = Release|Any CPU
346374
EndGlobalSection
347375
GlobalSection(SolutionProperties) = preSolution
348376
HideSolutionNode = FALSE
@@ -372,6 +400,8 @@ Global
372400
{12FB8C5C-E8B9-4E12-82E6-5C40500532D0} = {A3D60BFF-155C-404C-B6FC-B9B120B7D102}
373401
{E6A8F795-8E4C-44E4-9AAF-E2D14FDEF62C} = {A3D60BFF-155C-404C-B6FC-B9B120B7D102}
374402
{6F6CD966-35A6-4A56-8D4C-D87EEE383374} = {1286A675-A314-4874-95B6-A1C31A579F38}
403+
{C0CA0EB4-4B27-4C9D-8140-4837733D9FB7} = {A3D60BFF-155C-404C-B6FC-B9B120B7D102}
404+
{AD306A6F-B4A5-4AC0-B111-E3FF85DD16D6} = {A3D60BFF-155C-404C-B6FC-B9B120B7D102}
375405
EndGlobalSection
376406
GlobalSection(ExtensibilityGlobals) = postSolution
377407
SolutionGuid = {1573E771-2F69-48B2-A68A-6380B17F619C}

Diff for: Package-README.md

+307
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
# KubeClient
2+
3+
[![Build Status](https://dev.azure.com/tintoy-dev/dotnet-kube-client/_apis/build/status%2Ftintoy.dotnet-kube-client?branchName=refs%2Ftags%2Fv2.5.9)](https://dev.azure.com/tintoy-dev/dotnet-kube-client/_build/latest?definitionId=4&branchName=refs%2Ftags%2Fv2.5.9)
4+
5+
KubeClient is an extensible Kubernetes API client for .NET (targets `net8.0`).
6+
7+
Note - there is also an [official](https://github.com/kubernetes-client/csharp/) .NET client for Kubernetes (both clients actually share code in a couple of places). These two clients are philosophically-different (from a design perspective) but either can be bent to fit your needs. For more information about how KubeClient differs from the official client, see the section below on [extensibility](#extensibility).
8+
9+
## Packages
10+
11+
* `KubeClient` (`net8.0` or newer)
12+
The main client and models.
13+
[![KubeClient](https://img.shields.io/nuget/v/KubeClient.svg)](https://www.nuget.org/packages/KubeClient)
14+
* `KubeClient.Extensions.Configuration` (`net8.0` or newer)
15+
Support for sourcing `Microsoft.Extensions.Configuration` data from Kubernetes Secrets and ConfigMaps.
16+
[![KubeClient.Extensions.KubeConfig](https://img.shields.io/nuget/v/KubeClient.Extensions.Configuration.svg)](https://www.nuget.org/packages/KubeClient.Extensions.Configuration)
17+
* `KubeClient.Extensions.DependencyInjection` (`net8.0` or newer)
18+
Dependency-injection support.
19+
[![KubeClient.Extensions.KubeConfig](https://img.shields.io/nuget/v/KubeClient.Extensions.DependencyInjection.svg)](https://www.nuget.org/packages/KubeClient.Extensions.DependencyInjection)
20+
* `KubeClient.Extensions.KubeConfig` (`net8.0` or newer)
21+
Support for loading and parsing configuration from `~/.kube/config`.
22+
[![KubeClient.Extensions.KubeConfig](https://img.shields.io/nuget/v/KubeClient.Extensions.KubeConfig.svg)](https://www.nuget.org/packages/KubeClient.Extensions.KubeConfig)
23+
* `KubeClient.Extensions.WebSockets` (`net8.0` or newer)
24+
Support for multiplexed WebSocket connections used by Kubernetes APIs (such as [exec](src/KubeClient.Extensions.WebSockets/ResourceClientWebSocketExtensions.cs#L56)).
25+
This package also extends resource clients to add support for those APIs.
26+
27+
If you want to use the latest (development) builds of KubeClient, add the following feed to `NuGet.config`: https://www.myget.org/F/dotnet-kube-client/api/v3/index.json
28+
29+
## Usage
30+
31+
The client can be used directly or injected via `Microsoft.Extensions.DependencyInjection`.
32+
33+
### Use the client directly
34+
35+
The simplest way to create a client is to call `KubeApiClient.Create()`. There are overloads if you want to provide an access token, client certificate, or customise validation of the server's certificate:
36+
37+
```csharp
38+
// Assumes you're using "kubectl proxy", and no authentication is required.
39+
KubeApiClient client = KubeApiClient.Create("http://localhost:8001");
40+
41+
PodListV1 pods = await client.PodsV1().List(
42+
labelSelector: "k8s-app=my-app"
43+
);
44+
```
45+
46+
For more flexible configuration, use the overload that takes `KubeClientOptions`:
47+
48+
```csharp
49+
KubeApiClient client = KubeApiClient.Create(new KubeClientOptions
50+
{
51+
ApiEndPoint = new Uri("http://localhost:8001"),
52+
AuthStrategy = KubeAuthStrategy.BearerToken,
53+
AccessToken = "my-access-token",
54+
AllowInsecure = true // Don't validate server certificate
55+
});
56+
```
57+
58+
You can enable logging of requests and responses by passing an `ILoggerFactory` to `KubeApiClient.Create()` or `KubeClientOptions.LoggerFactory`:
59+
60+
```csharp
61+
ILoggerFactory loggers = new LoggerFactory();
62+
loggers.AddConsole();
63+
64+
KubeApiClient client = KubeApiClient.Create("http://localhost:8001", loggers);
65+
```
66+
67+
### Configure the client from ~/.kube/config
68+
69+
```csharp
70+
using KubeClient.Extensions.KubeConfig;
71+
72+
KubeClientOptions clientOptions = K8sConfig.Load(kubeConfigFile).ToKubeClientOptions(
73+
kubeContextName: "my-cluster",
74+
defaultKubeNamespace: "kube-system"
75+
);
76+
77+
KubeApiClient client = KubeApiClient.Create(clientOptions);
78+
```
79+
80+
### Make the client available for dependency injection
81+
82+
The client can be configured for dependency injection in a variety of ways.
83+
84+
To use a fixed set of options for the client, use the overload of `AddKubeClient()` that takes `KubeClientoptions`:
85+
86+
```csharp
87+
void ConfigureServices(IServiceCollection services)
88+
{
89+
services.AddKubeClient(new KubeClientOptions
90+
{
91+
ApiEndPoint = new Uri("http://localhost:8001"),
92+
AuthStrategy = KubeAuthStrategy.BearerToken,
93+
AccessToken = "my-access-token",
94+
AllowInsecure = true // Don't validate server certificate
95+
});
96+
}
97+
```
98+
99+
To add a named instance of the client:
100+
101+
```csharp
102+
void ConfigureServices(IServiceCollection services)
103+
{
104+
services.AddNamedKubeClients();
105+
services.AddKubeClientOptions("my-cluster", clientOptions =>
106+
{
107+
clientOptions.ApiEndPoint = new Uri("http://localhost:8001");
108+
clientOptions.AuthStrategy = KubeAuthStrategy.BearerToken;
109+
clientOptions.AccessToken = "my-access-token";
110+
clientOptions.AllowInsecure = true; // Don't validate server certificate
111+
});
112+
113+
// OR:
114+
115+
services.AddKubeClient("my-cluster", clientOptions =>
116+
{
117+
clientOptions.ApiEndPoint = new Uri("http://localhost:8001");
118+
clientOptions.AuthStrategy = KubeAuthStrategy.BearerToken;
119+
clientOptions.AccessToken = "my-access-token";
120+
clientOptions.AllowInsecure = true; // Don't validate server certificate
121+
});
122+
}
123+
124+
// To use named instances of KubeApiClient, inject INamedKubeClients.
125+
126+
class MyClass
127+
{
128+
public MyClass(INamedKubeClients namedKubeClients)
129+
{
130+
KubeClient1 = namedKubeClients.Get("my-cluster");
131+
KubeClient2 = namedKubeClients.Get("another-cluster");
132+
}
133+
134+
IKubeApiClient KubeClient1 { get; }
135+
IKubeApiClient KubeClient2 { get; }
136+
}
137+
```
138+
139+
## Design philosophy
140+
141+
Use of code generation is limited; generated clients tend to wind up being non-idiomatic and, for a Swagger spec as large as that of Kubernetes, wind up placing too many methods directly on the client class.
142+
143+
KubeClient's approach is to generate model classes (see `src/swagger` for the Python script that does this) and hand-code the actual operation methods to provide an improved consumer experience (i.e. useful and consistent exception types).
144+
145+
### KubeResultV1
146+
147+
Some operations in the Kubernetes API can return a different response depending on the arguments passed in. For example, a request to delete a `v1/Pod` returns the existing `v1/Pod` (as a `PodV1` model) if the caller specifies `DeletePropagationPolicy.Foreground` but returns a `v1/Status` (as a `StatusV1` model) if any other type of `DeletePropagationPolicy` is specified.
148+
149+
To handle this type of polymorphic response KubeClient uses the `KubeResultV1` model (and its derived implementations, `KubeResourceResultV1<TResource>` and `KubeResourceListResultV1<TResource>`).
150+
151+
`KubeResourceResultV1<TResource>` can be implicitly cast to a `TResource` or a `StatusV1`, so consuming code can continue to use the client as if it expects an operation to return only a resource or expects it to return only a `StatusV1`:
152+
153+
```csharp
154+
PodV1 existingPod = await client.PodsV1().Delete("mypod", propagationPolicy: DeletePropagationPolicy.Foreground);
155+
// OR:
156+
StatusV1 deleteStatus = await client.PodsV1().Delete("mypod", propagationPolicy: DeletePropagationPolicy.Background);
157+
```
158+
159+
If an attempt is made to cast a `KubeResourceResultV1<TResource>` that contains a non-success `StatusV1` to a `TResource`, a `KubeApiException` is thrown, based on the information in the `StatusV1`:
160+
161+
```csharp
162+
PodV1 existingPod;
163+
164+
try
165+
{
166+
existingPod = await client.PodsV1().Delete("mypod", propagationPolicy: DeletePropagationPolicy.Foreground);
167+
}
168+
catch (KubeApiException kubeApiError)
169+
{
170+
Log.Error(kubeApiError, "Failed to delete Pod: {ErrorMessage}", kubeApiError.Status.Message);
171+
}
172+
```
173+
174+
For more information about the behaviour of `KubeResultV1` and its derived implementations, see [KubeResultTests.cs](test/KubeClient.Tests/KubeResultTests.cs).
175+
176+
## Extensibility
177+
178+
KubeClient is designed to be easily extensible. The `KubeApiClient` provides the top-level entry point for the Kubernetes API and extension methods are used to expose more specific resource clients.
179+
180+
Simplified version of [PodClientV1.cs](src/KubeClient/ResourceClients/PodClientV1.cs):
181+
182+
```csharp
183+
public class PodClientV1 : KubeResourceClient
184+
{
185+
public PodClientV1(KubeApiClient client) : base(client)
186+
{
187+
}
188+
189+
public async Task<List<PodV1>> List(string labelSelector = null, string kubeNamespace = null, CancellationToken cancellationToken = default)
190+
{
191+
PodListV1 matchingPods =
192+
await Http.GetAsync(
193+
Requests.Collection.WithTemplateParameters(new
194+
{
195+
Namespace = kubeNamespace ?? KubeClient.DefaultNamespace,
196+
LabelSelector = labelSelector
197+
}),
198+
cancellationToken: cancellationToken
199+
)
200+
.ReadContentAsObjectV1Async<PodListV1>();
201+
202+
return matchingPods.Items;
203+
}
204+
205+
public static class Requests
206+
{
207+
public static readonly HttpRequest Collection = KubeRequest.Create("api/v1/namespaces/{Namespace}/pods?labelSelector={LabelSelector?}&watch={Watch?}");
208+
}
209+
}
210+
```
211+
212+
Simplified version of [ClientFactoryExtensions.cs](src/KubeClient/ClientFactoryExtensions.cs#L97):
213+
214+
```csharp
215+
public static PodClientV1 PodsV1(this KubeApiClient kubeClient)
216+
{
217+
return kubeClient.ResourceClient(
218+
client => new PodClientV1(client)
219+
);
220+
}
221+
```
222+
223+
This enables the following usage of `KubeApiClient`:
224+
225+
```csharp
226+
KubeApiClient client;
227+
PodListV1 pods = await client.PodsV1().List(kubeNamespace: "kube-system");
228+
```
229+
230+
Through the use of extension methods, resource clients (or additional operations) can be declared in any assembly and used as if they are part of the `KubeApiClient`. For example, the `KubeClient.Extensions.WebSockets` package adds an `ExecAndConnect` method to `PodClientV1`.
231+
232+
Simplified version of [ResourceClientWebSocketExtensions.cs](src/KubeClient.Extensions.WebSockets/ResourceClientWebSocketExtensions.cs#L56):
233+
234+
```csharp
235+
public static async Task<K8sMultiplexer> ExecAndConnect(this IPodClientV1 podClient, string podName, string command, bool stdin = false, bool stdout = true, bool stderr = false, bool tty = false, string container = null, string kubeNamespace = null, CancellationToken cancellation = default)
236+
{
237+
byte[] outputStreamIndexes = stdin ? new byte[1] { 0 } : new byte[0];
238+
byte[] inputStreamIndexes;
239+
if (stdout && stderr)
240+
inputStreamIndexes = new byte[2] { 1, 2 };
241+
else if (stdout)
242+
inputStreamIndexes = new byte[1] { 1 };
243+
else if (stderr)
244+
inputStreamIndexes = new byte[1] { 2 };
245+
else if (!stdin)
246+
throw new InvalidOperationException("Must specify at least one of STDIN, STDOUT, or STDERR.");
247+
else
248+
inputStreamIndexes = new byte[0];
249+
250+
return await podClient.KubeClient
251+
.ConnectWebSocket("api/v1/namespaces/{KubeNamespace}/pods/{PodName}/exec?stdin={StdIn?}&stdout={StdOut?}&stderr={StdErr?}&tty={TTY?}&command={Command}&container={Container?}", new
252+
{
253+
PodName = podName,
254+
Command = command,
255+
StdIn = stdin,
256+
StdOut = stdout,
257+
StdErr = stderr,
258+
TTY = tty,
259+
Container = container,
260+
KubeNamespace = kubeNamespace ?? podClient.KubeClient.DefaultNamespace
261+
}, cancellation)
262+
.Multiplexed(inputStreamIndexes, outputStreamIndexes,
263+
loggerFactory: podClient.KubeClient.LoggerFactory()
264+
);
265+
}
266+
```
267+
268+
Example usage of `ExecAndConnect`:
269+
270+
```csharp
271+
KubeApiClient client;
272+
K8sMultiplexer connection = await client.PodsV1().ExecAndConnect(
273+
podName: "my-pod",
274+
command: "/bin/bash",
275+
stdin: true,
276+
stdout: true,
277+
tty: true
278+
);
279+
using (connection)
280+
using (StreamWriter stdin = new StreamWriter(connection.GetOutputStream(0), Encoding.UTF8))
281+
using (StreamReader stdout = new StreamReader(connection.GetInputStream(1), Encoding.UTF8))
282+
{
283+
await stdin.WriteLineAsync("ls -l /");
284+
await stdin.WriteLineAsync("exit");
285+
286+
// Read from STDOUT until process terminates.
287+
string line;
288+
while ((line = await stdout.ReadLineAsync()) != null)
289+
{
290+
Console.WriteLine(line);
291+
}
292+
}
293+
```
294+
295+
For information about `HttpRequest`, `UriTemplate`, and other features used to implement the client take a look at the [HTTPlease](https://tintoy.github.io/HTTPlease/) documentation.
296+
297+
### Working out what APIs to call
298+
299+
If you want to replicate the behaviour of a `kubectl` command you can pass the flag `--v=10` to `kubectl` and it will dump out (for each request that it makes) the request URI, request body, and response body.
300+
301+
### Building
302+
303+
You will need to use v8.0.400 (or newer) of the .NET SDK to build KubeClient.
304+
305+
## Questions / feedback
306+
307+
Feel free to [get in touch](https://github.com/tintoy/dotnet-kube-client/issues/new) if you have questions, feedback, or would like to contribute.

Diff for: src/Common.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<!-- Common package properties -->
99
<PropertyGroup>
10-
<PackageReadmeFile>README.md</PackageReadmeFile>
10+
<PackageReadmeFile>Package-README.md</PackageReadmeFile>
1111
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1212

1313
<PackageProjectUrl>https://github.com/tintoy/dotnet-kube-client/</PackageProjectUrl>

Diff for: src/KubeClient.Core/AssemblyVisiblity.cs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("KubeClient.Tests")]
File renamed without changes.
File renamed without changes.

Diff for: src/KubeClient.Core/KubeClient.Core.csproj

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>net7.0;net8.0;net9.0;netstandard2.1</TargetFrameworks>
4+
5+
<RootNamespace>KubeClient</RootNamespace>
6+
7+
<Description>Common functionality for KubeClient (dotnet-kube-client)</Description>
8+
</PropertyGroup>
9+
10+
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.1'">
11+
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
12+
</ItemGroup>
13+
14+
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
15+
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
16+
</ItemGroup>
17+
18+
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
19+
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
20+
</ItemGroup>
21+
22+
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
23+
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
24+
</ItemGroup>
25+
26+
<Import Project="../Common.props" />
27+
</Project>

0 commit comments

Comments
 (0)