Contents

swiftkube/model

## Table of contents

Table of contents

Examples Builders Extensions Type-erasure

Overview

SwiftkubeModel is a zero-dependency Swift package for Kubernetes API objects.

  • [x] Model structs for all Kubernetes objects
  • [x] Codable support
  • [x] Hashable resources
  • [x] Sendable support
  • [x] Closure-based builders for convenient object composition
  • [x] Type-erased wrappers for Kubernetes resources
  • [x] UnstructuredResource type for handling any Kubernetes resource

Compatibility Matrix

| | 1.26.4 | 1.28.0 | 1.28.3 | 1.29.6 | 1.32.0 | 1.32.2 | 1.33.3 | 1.34.6 | |----------|--------|--------|--------|--------|--------|--------|--------|--------| | 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 | - | - | - | - | - | - | ✓ | - | | 0.19.x | - | - | - | - | - | - | - | ✓ |

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

Usage

To use the Kubernetes objects just import `SwiftkubeModel`: 

```swift
import SwiftkubeModel

let metadata = meta.v1.ObjectMatadata(name: "swiftkube")
let pod = core.v1.Pod(metadata: metadata)
```

All the objects are namespaced according to their API group and version, e.g. `apps.v1.Deployment` or 
`networking.v1beta1.Ingress`. Which means, that for example `rbac.v1.Role` and `rbac.v1beta1.Role` are completely 
different objects.

### Examples

Any Kubernetes object can be constructed directly using the model structs. Here is an example for a `Deployment` manifest:

```swift
let deployment = apps.v1.Deployment(
    metadata: meta.v1.ObjectMeta(
        name: "nginx"
    ),
    spec: apps.v1.DeploymentSpec(
        replicas: 1,
        selector: meta.v1.LabelSelector(
            matchLabels: ["app": "nginx"]
        ),
        template: core.v1.PodTemplateSpec (
            spec: core.v1.PodSpec(
                containers: [
                    core.v1.Container(
                        image: "nginx",
                        name: "nginx",
                    )
                ]
            )
        )
    )
)
```

Here is a `ConfigMap`:

```swift
let configMap = core.v1.ConfigMap(
    metadata: meta.v1.ObjectMeta(
        name: "config"
    ),
    data: [
        "env": "dev",
        "log_leve": "debug"
    ]
)
```

A more complete example of a `Deployment`, that defines `Probes`, `ResourceRequirements`, `Volumes` and `VolumeMounts` 
would look something like this:

```swift
let deployment = apps.v1.Deployment(
    metadata: meta.v1.ObjectMeta(
        name: "opa",
        namespace: "default"
    ),
    spec: apps.v1.DeploymentSpec(
        replicas: 2,
        selector: meta.v1.LabelSelector(
            matchLabels: ["app": "opa"]
        ),
        template: core.v1.PodTemplateSpec (
            spec: core.v1.PodSpec(
                containers: [
                    core.v1.Container(
                        image: "openpolicyagent/opa",
                        name: "opa",
                        readinessProbe: core.v1.Probe(
                            failureThreshold: 1,
                            httpGet: core.v1.HTTPGetAction(
                                path: "/health",
                                port: 8080
                            ),
                            initialDelaySeconds: 10,
                            periodSeconds: 20,
                            successThreshold: 2,
                            timeoutSeconds: 5
                        ),
                        resources: core.v1.ResourceRequirements(
                            limits: [
                                "ram": "512MB"
                            ],
                            requests: [
                                "ram": "128MB",
                                "cpu": "200m",
                            ]
                        ),
                        volumeMounts: [
                            core.v1.VolumeMount(
                                mountPath: "/etc/test",
                                name: "data"
                            )
                        ]
                    )
                ],
                imagePullSecrets: [
                    core.v1.LocalObjectReference(name: "secret-name")
                ],
                volumes: [
                    core.v1.Volume(
                        name: "data",
                        persistentVolumeClaim: core.v1.PersistentVolumeClaimVolumeSource(
                            claimName: "pvc",
                            readOnly: true
                        )
                    )
                ]
            )
        )
    )
)
```

### Sendables

All resources are `Sendable` structs.

There is, however, one caveat when working with resources having a `JSONObject` field:

- `apiextensions.v1.CustomResourceValidation`
- `apps.v1.ControllerRevision`
- `meta.v1.ManagedFieldsEntry`
- `meta.v1.WatchEvent`
- `resource.v1alpha3.AllocatedDeviceStatus`
- `resource.v1alpha3.OpaqueDeviceConfiguration`

or when working with `UnstructuredResource`.

They store their properties as `Dictionary<String, any Sendable>`. Thus, dictionary literals must be
explicitly cast to `[String: any Sendable]`.

For example:

```swift
UnstructuredResource(properties: [
    "apiVersion": "v1",
    "kind": "ConfigMap",
    "metadata": meta.v1.ObjectMeta(name: "configs", namespace: "default"),
    "data": [
        "foo": 42,
	    "bar": "baz"
    ] as [String: any Sendable]
])
```


### Builders

From the above example it is clear, that a certain knowledge of all the subtypes and their API groups is required, in 
order to comose a complete manifest. Furthermore, Swift doesn't allow arbitrary arguments order.

For this purpose `SwiftkubeModel` provides simple closure-based builder functions for convenience. All these functions 
reside under the `sk` namespace.

> :warning: The syntax is not yet finalized and can break many times before v1.0.0 ships. This can also be replaced 
> with Function/Result Builders, which is currently a WIP.


> :warning: `SwiftkubeModel` currently provides convenience builders only for the most common Kubernetes objects.

The above example would look like this:

```swift
let deployment = sk.deployment(name: "opa") {
    $0.metadata = sk.metadata {
        $0.namespace = "default"
    }
    $0.spec = sk.deploymentSpec {
        $0.replicas = 1
        $0.selector = sk.match(labels: ["app": "nginx"])
        $0.template = sk.podTemplate {
            $0.spec = sk.podSpec {
                $0.containers = [
                    sk.container(name: "opa") {
                        $0.image = "openpolicyagent/opa"
                        $0.readinessProbe = sk.probe(action: .httpGet(path: "/health", port: 8080)) {
                            $0.failureThreshold = 1
                            $0.initialDelaySeconds = 10
                            $0.periodSeconds = 20
                            $0.successThreshold = 2
                            $0.failureThreshold = 5
                        }
                        $0.resources = sk.requirements {
                            $0.requests = [
                                "ram": "512MB"
                            ]
                            $0.limits = [
                                "ram": "128MB",
                                "cpu": "200m",
                            ]
                        }
                        $0.volumeMounts = [
                            sk.volumeMount(name: "data", mountPath: "/etc/test")
                        ]
                    }
                ]
                $0.imagePullSecrets = [
                    sk.localObjectReference(name: "secret-name")
                ]
                $0.volumes = [
                    sk.volume(name: "data", from: .persistentVolumeClaim(claimName: "pvc", readOnly: true))
                ]
            }
        }
    }
}
```

### Extensions

In addition to closure-based builders, `SwiftkubeModel` extends the Model objects with some convenience functions, 
*inspired by [cdk8s](https://cdk8s.io)*

#### core.v1.ConfigMap

- Populating a `ConfigMap`

```swift
let configMap: core.v1.ConfigMap = sk.configMap(name: "test")

// populate the config map
configMap.add(data: "stuff", forKey: "foo")
configMap.add(binaryData: <binary>, forKey: "foo")
configMap.add(file: URL(fileURLWithPath: "/some/path"), forKey: "foo")
configMap.add(binaryFile: URL(fileURLWithPath: "/some/path"), forKey: "foo")
```

### core.v1.Container

- Mount a volume in a container

```swift
let container: core.v1.Container = ...
let volume: core.v1.Volume = ...

// mount a volume in a container
container.mount(volume: volume, on: "/data")
container.mount(volume: "dataVolume", on: "/data")
```

### core.v1.Namespace

- Finalizers


```swift
let namespace: core.v1.Namespace = ...

// add/remove finalizers
namespace.add(finalizer: "foo")
namespace.remove(finalizer: "foo")
```

#### core.v1.Secret

- Populating a `Secret`: the values are Base64-encoded automatically

```swift
let secret: core.v1.Secret = sk.secret(name: "test")

// populate the secret
configMap.add(data: "stuff", forKey: "foo")
configMap.add(file: URL(fileURLWithPath: "/some/path"), forKey: "foo")
```

#### core.v1.Service

- Server ports on a service

```swift
let service: core.v1.Service = ...

// add a service port entry
service.serve(port: 8080, targetPort: 80)
```

#### core.v1.ServiceAccount

- Use secrets


```swift
let serviceAccount: core.v1.ServiceAccount = ...

// add an object reference for a secret
serviceAccount.use(imagePullSecret: "pullSecret")
serviceAccount.use(secret: "secret", namespace: "ns")
```

#### apps.v1.Deployment

- Exposing a `Deployment`

```swift
let deployment: apps.v1.Deployment = ...

// expose a deployment instance to create a service
let service = deployment.expose(on: 8080, type: .clusterIP)
```

### Type-erasure

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.

`SwiftkubeModel` provides a type-erased resource implementation `UnstructuredResource` and its corresponding 
List-Type `UnstructuredResourceList` in order to tackle these use-cases.

`UnstruturedResource` allows objects that do not have registered `KubernetesAPIResource`s to be manipulated generically.
This can be used to deal with the API objects from a plug-in or CRDs.

Here are some examples to clarify their purpose:

```swift
// Given a JSON string, e.g. at runtime, containing some Kubernetes resource
let json = """
  {
    "apiVersion": "stable.example.com/v1",
    "kind": "CronTab",
    "metadata": {
      "name": "my-new-cron-object",
      "namespace": "default"
    },
     "spec": {
       "cronSpec": "* * * * */5",
       "image": "my-awesome-cron-image"
     }
  }
"""

// We can still decode it without knowing the concrete type
let data = str.data(using: .utf8)!
let resource = try? JSONDecoder().decode(UnstructuredResource.self, from: data)

// When encoding the previous instance, it serializes the underlying resource
let encoded = try? JSONEncoder().encode(resource)
```

The `UnstruturedResource` exposes its internal dictionary representation and also provides a dynamic subscript support:

```swift
let json = """
  {
    "apiVersion": "stable.example.com/v1",
    "kind": "CronTab",
    "metadata": {
      "name": "my-new-cron-object",
      "namespace": "default"
    },
     "spec": {
       "cronSpec": "* * * * */5",
       "image": "my-awesome-cron-image"
     }
  }
"""

let data = str.data(using: .utf8)!
let cron = try? JSONDecoder().decode(UnstructuredResource.self, from: data)

// Shortcut vars
print(cron.apiVersion)
print(cron.kind)
print(cron.metadata)

// The internal Dictionary<String: Any> representation
print(cron.properties)

// Dynamic member lookup
let spec: [String: Any]? = cron.spec
print(spec?["cronSpec"])
```

Installation

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

.package(name: "SwiftkubeModel", url: "https://github.com/swiftkube/model.git", from: "0.18.0")

then include it as a dependency in your target:

import PackageDescription

let package = Package(
    // ...
    dependencies: [
        .package(name: "SwiftkubeModel", url: "https://github.com/swiftkube/model.git", from: "0.18.0")
    ],
    targets: [
        .target(name: "<your-target>", dependencies: [
            .product(name: "SwiftkubeModel", package: "SwiftkubeModel"),
        ])
    ]
)

Then run swift build.

License

Swiftkube project is licensed under version 2.0 of the Apache License. See LICENSE for more details.

Package Metadata

Repository: swiftkube/model

Default branch: main

README: README.md