k8ts (pronounced “Kate’s”) is an experimental TypeScript framework for generating k8s manifests. It currently only has a TypeScript API.
🔗Reference tracking Tracks and validates references between resources, reducing the likelihood of deployment errors.
📝 Source tracking Links manifests to versions, commits, and lines of code using annotations. A single glance at a resource is enough to tell where it came from.
📂 File-based organization Lets you generate resources into separate files, ensuring the output is organized and human-readable.
Ideal for deployment using GitOps tools, such as FluxCD.
🧰 Useful APIs Handy abstractions for working with paths, command-lines, environment variables, URLs, and paths. Stuff that commonly appears in k8s manifests.
🗃️ Metadata management A rich, extensible metadata model that lets you easily embed metadata automatically.
🛠️ Transparent pipeline Uses a highly transparent declaration-to-serialization pipeline that can be tapped at any point to do things like:
- Filter resources
- Modify their specs
- Add labels or annotations
🧩 Highly extensible At its core, it’s a framework for building k8s generators.
- Uses decorators to capture common functionality.
- Rich dependency graph allows tracking of arbitrary resources.
😴 Doesn’t deploy anything K8ts builds manifest but deployment is handled using other tools, such as FluxCD. This is to be deliberately avoided.
🙈 Doesn’t look at the cluster Doesn’t run queries or check what resources are currently in the cluster.
Some sort of validation mechanism might be added in the future, but at its core, k8ts will always remain a framework for generating text files.
Currently three packages are needed:
yarn add -D k8ts @k8ts/instruments @k8ts/metadataThe topmost k8ts abstraction is the World, which captures everything that’s going to be generated.
You create a World like this:
import { World } from "k8ts"
export const W = World.make({
name: "everything"
})The World isn’t represented in the output. It also doesn’t actually generate output It’s just an organizational tool.
Worlds contains Files. These are actually the files that will be generated. Files have a name (always X.yaml) and can be either cluster-scoped or namespace-scoped.
Currently, Files are just objects. A single source file can declare multiple Files. However, its’ recommended to declare a single File as a default export.
Files contain resources emitted by a generator function.
import { W } from "./world"
export default W.Scope("cluster") // start with the scope
.File("namespace.yaml") // then name
.Resources(function* FILE(FILE) {
//
// contents
})The input to the generator function is a factory that can generate resources of the appropriate scope.
Since we chose to make a cluster-scoped file, we can only create cluster-scoped resources inside it. This creates separation between cluster- and namespace-scoped resources.
For example, we can create namespaces and PVs, but not Deployments or PVCs.
export default W.Scope("cluster")
.File("namespace.yaml")
.Resources(function* FILE(FILE) {
yield FILE.Namespace("namespace")
yield FILE.PersistentVolume("pv-cool", {
$capacity: "1Gi",
$accessModes: ["ReadWriteOnce"]
})
})The nesting structure is pretty deep, but it becomes simpler when you notice that each block is closed by }).
Fields are validated by TypeScript, including most fields that contain liters. So we can’t write a string like “xyz” for $capacity as it would error.