Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/swiftkube/client

Swift client for Kubernetes
https://github.com/swiftkube/client

kubernetes kubernetes-client server-side-swift swift swiftkube

Last synced: 10 days ago
JSON representation

Swift client for Kubernetes

Awesome Lists containing this project

README

        











Kubernetes 1.29.6


Swift Package Manager


CI Status

## Table of contents

* [Overview](#overview)
* [Compatibility Matrix](#compatibility-matrix)
* [Examples](#examples)
* [Usage](#usage)
* [Creating a client](#creating-a-client)
* [Configuring a client](#configuring-the-client)
* [Client authentication](#client-authentication)
* [Client DSL](#client-dsl)
* [Loading from external sources](#loading-from-external-sources)
* [Type-erased usage](#type-erased-usage)
* [CRD Support](#crd-support)
* [Metrics](#metrics)
* [Installation](#installation)
* [License](#license)

## Overview

Swift client for talking to a [Kubernetes](http://kubernetes.io/) cluster via a fluent DSL based
on [SwiftNIO](https://github.com/apple/swift-nio) and the [AysncHTTPClient](https://github.com/swift-server/async-http-client).

- [x] Covers all Kubernetes API Groups in v1.28.3
- [x] Automatic configuration discovery
- [x] DSL style API
- [x] For all API Groups/Versions
- [x] Generic client support
- [x] Swift-Logging support
- [x] Loading resources from external sources
- [x] from files
- [x] from URLs
- [x] Read Options
- [x] List Options
- [x] Delete Options
- [ ] PATCH API
- [x] `/scale` API
- [x] `/status` API
- [x] Resource watch support
- [x] Follow pod logs support
- [x] Discovery API
- [x] CRD support
- [ ] Controller/Informer support
- [x] Swift Metrics
- [ ] Complete documentation
- [ ] End-to-end tests

## Compatibility Matrix

| | 1.18.9 | 1.19.8 | 1.20.9 | 1.22.7 | 1.24.8 | 1.24.10 | 1.26.4 | 1.28.0 | 1.28.3 | 1.29.6 |
|-------------------|--------|--------|--------|--------|--------|---------|--------|--------|--------|--------|
| `0.6.x` | ✓ | - | - | - | - | - | - | - | - | - |
| `0.7.x...0.9.x` | - | ✓ | - | - | - | - | - | - | - | - |
| `0.10.x` | - | - | ✓ | - | - | - | - | - | - | - |
| `0.11.x` | - | - | - | ✓ | - | - | - | - | - | - |
| `0.12.x...0.13.x` | - | - | - | - | ✓ | - | - | - | - | - |
| `0.14.x` | - | - | - | - | - | ✓ | - | - | - | - |
| `0.15.x` | - | - | - | - | - | - | ✓ | - | - | - |
| `0.16.x` | - | - | - | - | - | - | - | ✓ | - | - |
| `0.17.x` | - | - | - | - | - | - | - | - | ✓ | - |
| `0.18.x` | - | - | - | - | - | - | - | - | - | ✓ |

- `✓` Exact match of API objects in both client and the Kubernetes version.
- `-` API objects mismatches either due to the removal of old API or the addition of new API. However, everything the
- client and Kubernetes have in common will work.

## Examples

Concrete examples for using the `Swiftkube` tooling reside in the [Swiftkube:Examples](https://github.com/swiftkube/examples)
repository.

## Usage

### Creating a client

To create a client just import `SwiftkubeClient` and init an instance.

```swift
import SwiftkubeClient

let client = try KubernetesClient()
```

You should shut down the `KubernetesClient` instance when you're done using it, which in turn shuts down the underlying
`HTTPClient`. Thus, you shouldn't call `client.syncShutdown()` before all requests have finished. You can also shut down
the client asynchronously in an async/await context or by providing a `DispatchQueue` for the completion callback.

```swift
// when finished close the client
try client.syncShutdown()

// async/await
try await client.shutdown()

// DispatchQueue
let queue: DispatchQueue = ...
client.shutdown(queue: queue) { (error: Error?) in
print(error)
}
```

### Configuring the client

The client tries to resolve a `kube config` automatically from different sources in the following order:

- Kube config file in the user's `$HOME/.kube/config` directory
- `ServiceAccount` token located at `/var/run/secrets/kubernetes.io/serviceaccount/token` and a mounted CA certificate,
- if it's running in Kubernetes.

Alternatively it can be configured manually, for example:

```swift
let caCert = try NIOSSLCertificate.fromPEMFile(caFile)
let authentication = KubernetesClientAuthentication.basicAuth(
username: "admin",
password: "admin"
)

let config = KubernetesClientConfig(
masterURL: "https://kubernetesmaster",
namespace: "default",
authentication: authentication,
trustRoots: NIOSSLTrustRoots.certificates(caCert),
insecureSkipTLSVerify: false,
timeout: HTTPClient.Configuration.Timeout.init(connect: .seconds(1), read: .seconds(10)),
redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration.follow(max: 5, allowCycles: false)
)

let client = KubernetesClient(config: config)
```

### Client authentication

The following authentication schemes are supported:

- Basic Auth: `.basicAuth(username: String, password: String)`
- Bearer Token: `.bearer(token: String)`
- Client certificate: `.x509(clientCertificate: NIOSSLCertificate, clientKey: NIOSSLPrivateKey)`

### Client DSL

`SwiftkubeClient` defines convenience API to work with Kubernetes resources. Using this DSL is the same for all resources.

The client exposes asynchronous functions using the new Swift concurrency model.

#### List resources

```swift
let namespaces = try await client.namespaces.list()
let deployments = try await client.appsV1.deployments.list(in: .allNamespaces)
let roles = try await client.rbacV1.roles.list(in: .namespace("ns"))
```

You can filter the listed resources or limit the returned list size via the `ListOptions`:

```swift
let deployments = try await client.appsV1.deployments.list(in: .allNamespaces, options: [
.labelSelector(.eq(["app": "nginx"])),
.labelSelector(.notIn(["env": ["dev", "staging"]])),
.labelSelector(.exists(["app", "env"])),
.fieldSelector(.eq(["status.phase": "Running"])),
.resourceVersion("9001"),
.limit(20),
.timeoutSeconds(10)
])
```

#### Get a resource

```swift
let namespace = try await client.namespaces.get(name: "ns")
let deployment = try await client.appsV1.deployments.get(in: .namespace("ns"), name: "nginx")
let roles = try await client.rbacV1.roles.get(in: .namespace("ns"), name: "role")
```

You can also provide the following `ReadOptions`:

```swift
let deployments = try await client.appsV1.deployments.get(in: .allNamespaces, options: [
.pretty(true),
.exact(false),
.export(true)
])
```

#### Delete a resource

```swift
try await client.namespaces.delete(name: "ns")
try await client.appsV1.deployments.delete(in: .namespace("ns"), name: "nginx")
try await client.rbacV1.roles.delete(in: .namespace("ns"), name: "role")
```

You can pass an instance of `meta.v1.DeleteOptions` to control the behaviour of the delete operation:

```swift
let deletOptions = meta.v1.DeleteOptions(
gracePeriodSeconds: 10,
propagationPolicy: "Foreground"
)
try await client.pods.delete(in: .namespace("ns"), name: "nginx", options: deleteOptions)
```

#### Create and update a resource

Resources can be created/updated directly or via the convenience builders defined in [SwiftkubeModel](https://github.com/swiftkube/model)

```swift
// Create a resource instance and post it
let configMap = core.v1.ConfigMap(
metadata: meta.v1.ObjectMeta(name: "test"),
data: ["foo": "bar"]
)
try cm = try await client.configMaps.create(inNamespace: .default, configMap)

// Or inline via a builder
let pod = try await client.pods.create(inNamespace: .default) {
sk.pod {
$0.metadata = sk.metadata(name: "nginx")
$0.spec = sk.podSpec {
$0.containers = [
sk.container(name: "nginx") {
$0.image = "nginx"
}
]
}
}
}
```

#### Watch a resource

You can watch for Kubernetes events about specific objects via the `watch` API.

Watching resources opens a persistent connection to the API server. The connection is represented by a `SwiftkubeClientTask`
instance, that acts as an active "subscription" to the events stream.

The task instance must be started explicitly via ``SwiftkubeClientTask/start()``, which returns an
``AsyncThrowingStream``, that starts yielding items immediately as they are received from the Kubernetes API server.

> The async stream buffers its results if there are no active consumers. The ``AsyncThrowingStream.BufferingPolicy.unbounded``
buffering policy is used, which should be taken into consideration.

```swift
let task: SwiftkubeClientTask = client.pods.watch(in: .allNamespaces)
let stream = task.start()

for try await event in stream {
print(event)
}
```

You can also pass `ListOptions` to filter, i.e. select the required objects:

```swift
let options = [
.labelSelector(.eq(["app": "nginx"])),
.labelSelector(.exists(["env"]))
]

let task = client.pods.watch(in: .default, options: options)
```

The client reconnects automatically and restarts the watch upon encountering non-recoverable errors. The
reconnect-behaviour can be controlled by passing an instance of `RetryStrategy`.

The default strategy is 10 retry attempts with a fixed 5 seconds delay between each attempt. The initial delay is one
second. A jitter of 0.2 seconds is applied.

Passing `RetryStrategy.never` disables any reconnection attempts.

```swift
let strategy = RetryStrategy(
policy: .maxAttemtps(20),
backoff: .exponentiaBackoff(maxDelay: 60, multiplier: 2.0),
initialDelay = 5.0,
jitter = 0.2
)
let task = client.pods.watch(in: .default, retryStrategy: strategy)

for try await event in task.stream() {
print(event)
}
```

The task must be cancelled when it is no longer needed:

```swift
task.cancel()
```

#### Follow logs

The `follow` API resembles the `watch`, but instead of events, it emits the log lines.

:warning: The client does not reconnect on errors in `follow` mode.

```swift
let task = client.pods.follow(in: .default, name: "nginx", container: "app")

for try await line in task.start() {
print(line)
}

// The task can be cancelled later to stop following logs
task.cancel()
```

### Discovery

The client provides a discovery interface for the API server, which can be used to retrieve the server version, the API
groups and the API resources for a specific group version.

```swift
let version: Info = try await client.discovery.serverVersion()
let groups: meta.v1.APIGroupList = try await client.discovery.serverGroups()
let resources: meta.v1.APIResourceList = try await client.discovery.serverResources(forGroupVersion: "apps/v1")
```

### Loading from external sources

A resource can be loaded from a file or a URL:

```swift
// Load from URL, e.g. a file
let url = URL(fileURLWithPath: "/path/to/manifest.yaml")
let deployment = try apps.v1.Deployment.load(contentsOf: url)
```

### Type-erased usage

Often when working with Kubernetes the concrete type of the resource is not known or not relevant, e.g. when creating
resources from a YAML manifest file. Other times the type or kind of the resource must be derived at runtime given its
string representation.

Leveraging `SwiftkubeModel`'s type-erased resource implementations `UnstructuredResource` and its corresponding
List-Type `UnstructuredResourceList` it is possible to have a generic client instance, which must be initialized
with a `GroupVersionResource` type:

```swift
guard let gvr = try? GroupVersionResource(for: "deployment") else {
// handle this
}

// Get by name
let resource: UnstructuredResource = try await client.for(gvr: gvr).get(in: .default , name: "nginx")

// List all
let resources: UnstructuredResourceList = try await client.for(gvr: gvr).list(in: .allNamespaces)
```

#### GroupVersionKind & GroupVersionResource

A `GroupVersionKind` & `GroupVersionResource` can be initialized from:

- `KubernetesAPIResource` instance
- `KubernetesAPIResource` type
- Full API Group string
- Lower-cased singular resource kind
- Lower-cased plural resource name
- Lower-cased short resource name

```swift
let deployment = ..
let gvk = GroupVersionKind(of: deployment)
let gvr = GroupVersionResource(of: deployment)
let gvk = GroupVersionKind(of: apps.v1.Deployment.self)
let gvr = GroupVersionResource(for: "configmaps")
let gvk = GroupVersionKind(for: "cm")
let gvr = GroupVersionResource(for: "cm")
// etc.
```

### CRD Support

`SwiftkubeClient` supports Custom Resource Definitions (CRDs) natively. For example, a CRD manifest can be loaded
from a YAML file or created programmatically, and then created via the client DSL:

```swift
let crd = apiextensions.v1.CustomResourceDefinition.load(contentsOf: URL(filePath: "/path/to/crd.yaml"))
try await client.apiExtensionsV1.customResourceDefinitions.create(crd)
```

The `KubernetesClient` can now be "extended", in order to manage the Custom Resources. One way would be to use the
`UnstructuredResource` described in the previous section given some `GroupVersionResource`.

However, the client can work with any object that implement the relevant marker protocols, which allows for custom types
to be defined and used directly.

Here is a complete example to clarify.

Given the following CRD:

```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: crontabs.example.com
spec:
group: example.com
names:
plural: crontabs
singular: crontab
kind: CronTab
shortNames:
- ct
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
cronSpec:
type: string
image:
type: string
replicas:
type: integer
```

The marker protocols are:

- `KubernetesAPIResource` marks the object as a Kubernetes resource that has a corresponding API endpoint
- `NamespacedResource` & `ClusterScopedResource` to indicate whether the resource is namespaced or cluster-scoped
- `ReadableResource` activates the `get`, `list` and `watch` API for the resource
- `CreatableResource` activates the `create` API for the resource
- `ReplaceableResource` activates the `update` API for the resource
- `DeletableResource` activates the `delete` API for the resource
- `CollectionDeletableResource` activate the `deleteAll` API for the resource
- `ScalableResource` activates the `scale` API for the resource
- `MetadataHavingResource` indicates, that the resource has a `metadata` field of type `meta.v1.ObjectMeta?`
- `StatusHavingResource` indicate, that the resource has a `scale` field (w/o assuming its type)

The following custom structs can be defined:

```swift
struct CronTab: KubernetesAPIResource, NamespacedResource, MetadataHavingResource,
ReadableResource, CreatableResource, ListableResource {
typealias List = CronTabList
var apiVersion = "example.com/v1"
var kind = "CronTab"
var metadata: meta.v1.ObjectMeta?
var spec: CronTabSpec
}

struct CronTabSpec: Codable, Hashable {
var cronSpec: String
var image: String
var replicas: Int
}

struct CronTabList: KubernetesResourceList {
var apiVersion = "example.com/v1"
var kind = "crontabs"
var items: [CronTab]
}
```

Now, the new Custom Resource can be used like any other Kubernetes resource:

```swift
let gvr = GroupVersionResource(
group: "example.com",
version: "v1",
resource: "crontabs"
)

let cronTabClient = client.for(CronTab.self, gvr: gvr)

let cronTab = CronTab(
metadata: meta.v1.ObjectMeta(name: "new-cron"),
spec: CronTabSpec(
cronSpec : "* * * * */5",
image: "some-cron-image",
replicas: 2
)
)

let new = try await cronTabClient.create(in: .default, cronTab)
let cronTabs: CronTabList = try await cronTabClient.list(in: .allNamespaces)
```

## Metrics

`KubernetesClient` uses [SwiftMetrics](https://github.com/apple/swift-metrics) to collect metric information about the
requests count and latencies.

The following metrics are gathered:

- `sk_http_requests_total(counter)`: the total count of the requests made by the client.
- `sk_http_request_errors_total(counter)`: the total number of requests made, that returned a http error.
- `sk_request_errors_total(counter)`: the total number of requests that couldn't be dispatched due to non-http errors.
- `sk_http_request_duration_seconds(timer)`: the complete request durations.

### Collecting the metrics

To collect the metrics you have to bootstrap a metrics backend in your application. For example, you can collect the
metrics to prometheus via `SwiftPrometheus`:

```swift
import Metrics
import Prometheus

let prom = PrometheusClient()
MetricsSystem.bootstrap(prom)
```

and expose a `/metrics` endpoint for scraping:

```swift
// if using vapor
app.get("metrics") { request -> EventLoopFuture in
let promise = request.eventLoop.makePromise(of: String.self)
try MetricsSystem.prometheus().collect(into: promise)
return promise.futureResult
}
```

## Installation

To use the `SwiftkubeClient` in a SwiftPM project, add the following line to the dependencies in your `Package.swift` file:

```swift
.package(name: "SwiftkubeClient", url: "https://github.com/swiftkube/client.git", from: "0.18.0")
```

then include it as a dependency in your target:

```swift
import PackageDescription

let package = Package(
// ...
dependencies: [
.package(name: "SwiftkubeClient", url: "https://github.com/swiftkube/client.git", from: "0.18.0")
],
targets: [
.target(name: "", dependencies: [
.product(name: "SwiftkubeClient", package: "SwiftkubeClient"),
])
]
)
```

Then run `swift build`.

## License

Swiftkube project is licensed under version 2.0 of the [Apache License](https://www.apache.org/licenses/LICENSE-2.0). See [LICENSE](./LICENSE) for more details.