Kubernetes Plugin Release Notes

0.1.6

Requires formae >= 0.86.0.

New features and improvements

Custom resources (CRD instances)

The plugin can now manage any custom resource — instances of CRDs like cert-manager Certificate, Argo Application, Flux GitRepository, etc. — with no per-CRD Go code or generated schema, through a generic catch-all type:

import "@k8s/v1.34/custom/CustomResource.pkl" as cr

new cr.CustomResource {
    apiVersion = "cert-manager.io/v1"
    kind = "Certificate"
    metadata { name = "web"; namespace = "default" }
    spec = new Dynamic {
        secretName = "web-tls"
        issuerRef = new Dynamic { name = "letsencrypt"; kind = "ClusterIssuer" }
    }
}

K8S::Custom::Resource reads apiVersion/kind from the manifest, resolves the GVR against the live cluster, and applies via Server-Side Apply — the same mechanics as the built-in typed resources. The body (spec and any top-level fields) is free-form, so it works for arbitrary CRD schemas. This is an escape-hatch model: no field-level validation or autocomplete, by design.

Identity is a composite formaeId (<apiVersion>/<kind>/<namespace>/<name>), unique across kinds since a single type spans every CRD.

CustomResourceDefinitions as a first-class type

K8S::Apiextensions::CustomResourceDefinition manages CRDs themselves (a CRD is just an apiextensions.k8s.io/v1 object). Backed by the same generic provisioner, it lets a CRD and an instance of the kind it defines live in one forma and deploy in a single formae apply — no kubectl bootstrap:

import "@k8s/v1.34/apiextensions/CustomResourceDefinition.pkl" as crd
import "@k8s/v1.34/custom/CustomResource.pkl" as cr

new crd.CustomResourceDefinition {
    metadata { name = "widgets.example.com" }
    spec = new Dynamic { /* group, names, scope, versions … */ }
}
new cr.CustomResource {
    apiVersion = "example.com/v1"
    kind = "Widget"
    metadata { name = "w1"; namespace = "default" }
    spec = new Dynamic { size = 3 }
}

The CRD provisioner blocks until the CRD's Established condition is True, and the instance's apply retries (re-discovering the RESTMapper) until its kind is served — so the two converge in one apply with no explicit ordering, and survive destroy/recreate cycles.

Discovery is currently disabled. Both K8S::Custom::Resource and K8S::Apiextensions::CustomResourceDefinition are marked discoverable = false. A single catch-all type spans every CRD kind, so unscoped discovery would pull every custom resource on the cluster (including operator-internal churn) into inventory. Discovery stays off until a scoped design lands; CRUD of explicitly-declared resources is fully supported.

Install operators with Helm, end-to-end

HelmChart now maps chart-rendered kinds that have no typed provisioner — the CRDs an operator ships — through K8S::Custom::Resource instead of skipping them. A single HelmChart therefore installs a complete operator: controllers and RBAC as typed resources, the operator's CRDs via the catch-all.

Bug fixes

Helm: workloads ordered after their ServiceAccount

Helm-rendered workloads set serviceAccountName as a plain string, so @formae applied Deployments concurrently with their ServiceAccounts — Pods then failed with serviceaccount not found until the SA caught up. The mapper now emits serviceAccountName as a resolvable referencing the SA resource, so @formae creates the ServiceAccount first and resolves the name. The reference round-trips to the SA name, so there is no drift. (The cluster-default default SA is left a plain string to avoid a dangling reference.)

Helm: invalid JobSpec.restartPolicy

The Helm batch mapper emitted restartPolicy at the JobSpec level, where the field does not exist (it belongs on the pod template). Charts that ship a Job (e.g. install hooks) now render correctly.

Discovery no longer errors on resource types newer than the target cluster

The plugin registers every resource type, but a type can be newer than the target cluster's Kubernetes version — e.g. MutatingAdmissionPolicy (GA in 1.36) on a 1.33 cluster. Discovery still called List for it, the apiserver returned the server could not find the requested resource, and it was logged on every discovery pass:

ERR Failed to list resources for K8S::Admissionregistration::MutatingAdmissionPolicy ... the server could not find the requested resource

Operations are now gated on whether the type is served by the target's Kubernetes version. The plugin resolves the target's version and, for a type that version doesn't serve, handles each operation accordingly — discovery List returns empty (no error), Create/Update fail with a clear message naming the type and version, Read/Status report not-found, and Delete is a no-op. Resolution is fail-safe: if the cluster version can't be determined, operations proceed as before.

0.1.4

Requires formae >= 0.86.0.

New features and improvements

K8s 1.36 conformance landed

kind v0.32.0 ships kindest/node:v1.36.1, so K8s 1.36 now runs the full conformance suite instead of being schema-only. The per-minor chain on main extends to 1.36 → 1.21, the PR conformance suite exercises 1.36 against kindest/node:v1.36.1, and the nightly cluster moves to the same image. This closes the 1.36 wire-up tracked in 0.1.3.

MutatingAdmissionPolicy

New resource type K8S::Admissionregistration::MutatingAdmissionPolicy (admissionregistration.k8s.io/v1, GA in K8s 1.36, KEP-3962) — the in-tree, CEL-based successor to mutating admission webhooks. It supports the full CRUD lifecycle and models matchConstraints, variables, matchConditions, and ApplyConfiguration / JSONPatch mutations.

import "@k8s/v1.36/admissionregistration/MutatingAdmissionPolicy.pkl" as map

new map.MutatingAdmissionPolicy {
    metadata = new k8s.ObjectMeta { name = "add-policy-label" }
    spec = new map.MutatingAdmissionPolicySpec {
        failurePolicy = "Ignore"
        matchConstraints = new map.MatchResources {
            resourceRules = new Listing {
                new map.NamedRuleWithOperations {
                    operations = new Listing { "CREATE" }
                    apiGroups = new Listing { "" }
                    apiVersions = new Listing { "v1" }
                    resources = new Listing { "pods" }
                }
            }
        }
        mutations = new Listing {
            new map.Mutation {
                patchType = "ApplyConfiguration"
                applyConfiguration = new map.ApplyConfiguration {
                    expression = "Object{ metadata: Object.metadata{ labels: {'formae.io/policy': 'managed'} } }"
                }
            }
        }
    }
}

The whole module is gated introducedIn = "1.36", so it materializes only in the @k8s/v1.36+ schema trees; referencing it with kubernetesVersion set to an earlier minor fails at pkl eval time. This raises the supported resource count to 36 types across 13 API groups.

Supported version window (clarification)

Two version ranges are in play, and they are not the same:

  • Schema trees ship for 1.21 → 1.36 (16 minors). These give you typed Pkl authoring and pkl eval-time field validation for that whole range.
  • Runtime support window is 1.31 → 1.36 (MinSupportedK8sVersion / MaxSupportedK8sVersion in pkg/config/version.go). Applying against a live cluster outside this window returns a clear preflight error.

So schema trees exist for 1.21 → 1.30, but those minors are below the runtime floor — you can author and eval against them, yet the plugin will refuse to drive a live cluster older than 1.31. The floor has been 1.31 since the per-version schema system was introduced; only the ceiling has moved (1.34 → 1.36, in lockstep with the pinned client-go). Targeting a cluster below 1.31 is not supported.

0.1.3

Requires formae >= 0.86.0.

New features and improvements

K8s 1.35 and 1.36 supported

The schema package now ships per-version subtrees for K8s minors 1.21 → 1.36 (16 minors). Target a specific cluster's API version through the kubernetesVersion field on the K8s Config:

new k8s.Config {
    kubernetesVersion = "1.36"
    auth = new k8s.KubeconfigAuth {}
}

Each per-version subtree under @k8s/v<X.Y>/ carries only fields that are valid for that minor; @formae evaluates against the matching subtree at extract and apply time, surfacing field-availability errors before any RPC reaches the cluster.

The client-go dependency is pinned to v0.36.0 in lockstep with the highest supported minor. The plugin's pkg/config/version.go records MinSupportedK8sVersion = "1.31" and MaxSupportedK8sVersion = "1.36"; users on a cluster outside that window get a clear preflight error.

Service LoadBalancer resolvables

core/Service now exposes its assigned LoadBalancer endpoint through resolvables: lbIngressIp, lbIngressHostname, and lbIngressUrl. The URL form is synthesized by the plugin as http://<host>[:port] from the first ingress address and the first service port, so a cross-plugin Target can take its endpoint directly from a $ref on the Service.

lgtm-observability example

New example composing three plugins in a single forma: a cloud plugin provisions a managed cluster (AWS, Azure, GCP, or OCI), the k8s plugin deploys the LGTM stack onto it, and the grafana plugin configures Grafana (folder, data sources, dashboards) over its HTTP API. Target chaining wires it together: the K8s target's auth is a $ref on the cluster endpoint, and the Grafana target's URL is a $ref on the Grafana Service's LoadBalancer ingress URL.

Versioned conformance CI

A new Conformance K8s 1.35 workflow runs on every push to main, joining the existing per-minor chain (1.35 → 1.34 → ... → 1.21). The PR conformance suite now exercises 1.35 against kindest/node:v1.35.1. K8s 1.36 conformance is tracked separately and will land once kind publishes a 1.36 node image; see issue #9 for the wire-up checklist.

Schema layout

The published schema/pkl/ tree now splits responsibility cleanly between the package root and the per-version subtrees:

schema/pkl/
├── target.pkl         # NEW root: Config + Auth (version-agnostic)
├── shared.pkl         # version-agnostic typealiases + K8sVersion annotation
├── helm/              # helm-chart wrappers (per-version + shared)
├── v1.21/
│   ├── k8s.pkl        # SubResource classes valid in 1.21
│   ├── apps/Deployment.pkl
│   ├── core/Pod.pkl
│   └── …
├── …
└── v1.36/
    ├── k8s.pkl        # SubResource classes valid in 1.36
    └── …
  • target.pkl is the version-agnostic package root. It carries Config (including kubernetesVersion) and the Auth hierarchy (KubeconfigAuth, EKSAuth, GKEAuth, AKSAuth, OVHAuth, OCIAuth).
  • v<X.Y>/k8s.pkl is the per-version SubResource module: PodSpec, Container, EnvVar, ObjectMeta, and every other inline type whose accepted field set varies per K8s minor. Each per-version file extends "../target.pkl", so importing @k8s/v1.34/k8s.pkl also gives you Config + Auth via inheritance.
  • Resource files (@k8s/v<X.Y>/<api-group>/<Kind>.pkl) sit under each per-version subtree and import the matching k8s.pkl for their subresource dependencies.

Authoring forma.pkl files against the new layout:

import "@k8s/target.pkl" as k8s
import "@k8s/v1.34/core/Namespace.pkl" as ns
import "@k8s/v1.34/apps/Deployment.pkl" as dep

new k8s.Config { kubernetesVersion = "1.34"; auth = new k8s.KubeconfigAuth {} }

Bug fixes

Example formae files

The example forma.pkl files under examples/formations/ now import @k8s/k8s-subresources.pkl as k8s against the master schema, restoring chart-conformance test compatibility that broke during the earlier schema split. Per-version example files (examples/helm/{nginx,memcached,postgresql}-v1.{31,34}.pkl) import the per-version @k8s/v<X.Y>/k8s.pkl subresources file.

0.1.2

Initial release of the Kubernetes plugin as a standalone package on the platform.engineering Hub. Manages K8s resources via Server-Side Apply with typed Pkl schemas pinned to your cluster's K8s version.