|
| 1 | +# KubeClient |
| 2 | + |
| 3 | +[](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 | + [](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 | + [](https://www.nuget.org/packages/KubeClient.Extensions.Configuration) |
| 17 | +* `KubeClient.Extensions.DependencyInjection` (`net8.0` or newer) |
| 18 | + Dependency-injection support. |
| 19 | + [](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 | + [](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. |
0 commit comments