Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bases and environment overlays

Cleat replaces Kustomize bases and overlays with modules and typed patches. A module holds the shared pieces; each environment imports it and patches the result.

A shared base

A module is a .cleat file that defines types, functions and constants. It can import schemas and refine types, but it can’t declare inputs or export anything: it takes values as function parameters instead.

// examples/overlays/base.cleat
import * as k8s from "../../schemas/kubernetes-1.37.json"

// Project policy: no NodePort or ExternalName Services.
refine k8s.ServiceSpec with { type: "ClusterIP" | "LoadBalancer" }

type DnsLabel = Str where len(self) <= 63 && matches(self, "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
type Port     = Int where 1 <= self && self <= 65535

fn labels(app: Str) -> { [Str]: Str } = {
  "app.kubernetes.io/name": app,
  "app.kubernetes.io/part-of": "shop",
}

/// A Deployment and a Service for one HTTP app.
fn app(name: DnsLabel, image: Str, port: Port = 8080) -> [k8s.Deployment | k8s.Service] = [
  k8s.Deployment {
    metadata: { name, labels: labels(name) },
    spec: {
      replicas: 1,
      selector.matchLabels: labels(name),
      template: {
        metadata.labels: labels(name),
        spec.containers: [{ name, image, ports: [{ name: "http", containerPort: port }] }],
      },
    },
  },
  k8s.Service {
    metadata: { name, labels: labels(name) },
    spec: { selector: labels(name), ports: [{ name: "http", port: 80, targetPort: "http" }] },
  },
]

An overlay

// examples/overlays/prod.cleat
import * as base from "./base.cleat"

input image: Str

export api = base.app("api", image) with {
  [kind: "Deployment"].spec.replicas: 3,
  [kind: "Deployment"].metadata.labels +: { tier: "prod" },
  [kind: "Deployment"].spec.template.spec.containers[name: "api"].resources.limits: { cpu: "1", memory: "512Mi" },
  [kind: "Deployment"].spec.template.spec.containers[name: "api"].env +: [{ name: "LOG_LEVEL", value: "warn" }],
  [kind: "Service"].spec.type: "LoadBalancer",
}
EntryEffect
path: vSet the value at path, creating missing objects on the way.
path +: vMerge: lists append; objects follow JSON Merge Patch (nested objects merge, null removes a key).
-pathRemove a field, or the selected list elements.
Path segmentSelects
.fa field
[]every list element or map value
[2]one list element
[name: "api", ...]the list elements whose fields equal these values

Mistakes are errors, not no-ops

Patches into declared types (like k8s.Deployment) are checked against them, and a selector that matches nothing is an error. The base’s refine applies to the overlay too:

import * as base from "./base.cleat"
input image: Str
export api = base.app("api", image) with {
  [kind: "Deployment"].spec.replcias: 3,
  [kind: "Service"].spec.type: "NodePort",
}
$ cleat check prod.cleat
prod.cleat:4:3: error: `[kind: "Deployment"].spec.replcias`: `base.k8s.apps.v1.DeploymentSpec` has no field `replcias`; did you mean `replicas`?
prod.cleat:5:32: error: type mismatch: expected `"ClusterIP" | "LoadBalancer"`, found `"NodePort"`
2 error(s), 0 warning(s); refinements: 1 proved statically, 0 checked at runtime

In anonymous objects (no declared type), a patch updates the type structurally instead: { n: 1 } with { n: "one" } has type { n: "one" }.