') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - swiftkube/model: Swift Kubernetes API objects · GitHub
Skip to content

Repository files navigation

Kubernetes 1.35.3Swift Package ManagerCI Status

Table of contents

Overview

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

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

Compatibility Matrix

1.28.01.28.31.29.61.32.01.32.21.33.31.34.61.35.3
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-------
0.20.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:

import SwiftkubeModel
letmetadata= meta.v1.ObjectMatadata(name:"swiftkube")letpod= 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:

letdeployment= 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:

letconfigMap= 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:

letdeployment= 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:

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

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.

⚠️ 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.

⚠️SwiftkubeModel currently provides convenience builders only for the most common Kubernetes objects.

The above example would look like this:

letdeployment= 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

core.v1.ConfigMap

  • Populating a ConfigMap
letconfigMap: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
letcontainer:core.v1.Container=...letvolume: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
letnamespace: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
letsecret: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
letservice:core.v1.Service=...
// add a service port entry
service.serve(port:8080, targetPort:80)

core.v1.ServiceAccount

  • Use secrets
letserviceAccount: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
letdeployment:apps.v1.Deployment=...
// expose a deployment instance to create a service
letservice= 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 KubernetesAPIResources 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:

// Given a JSON string, e.g. at runtime, containing some Kubernetes resource
letjson=""" {"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
letdata= str.data(using:.utf8)!
letresource=try?JSONDecoder().decode(UnstructuredResource.self, from: data)
// When encoding the previous instance, it serializes the underlying resource
letencoded=try?JSONEncoder().encode(resource)

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

letjson=""" {"apiVersion": "stable.example.com/v1","kind": "CronTab","metadata": {"name": "my-new-cron-object","namespace": "default" },"spec": {"cronSpec": "* * * * */5","image": "my-awesome-cron-image" } }"""letdata= str.data(using:.utf8)!
letcron=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
letspec:[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.20.0")

then include it as a dependency in your target:

import PackageDescription
letpackage=Package(
// ...
dependencies:[.package(name:"SwiftkubeModel", url:"https://github.com/swiftkube/model.git", from:"0.20.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.

Releases

Packages

Contributors

Languages