formae-plugin-kubernetes conforms to the API schema versioning contract, step by step. The same recipe applies to any plugin whose target API evolves per version: fields get introduced, graduate from alpha to GA, get renamed, or are removed between releases. For the mechanics of how formae core resolves versions and narrows imports, read the concept page API schema versioning first. This page is the task-oriented walkthrough.
Publish the per-version schema layout
This is theschema/pkl/ tree that ends up under ~/.pel/formae/plugins/k8s/ after formae plugin install. It’s also the package layout consumers see when they pull the package from hub.platform.engineering.
- No top-level
k8s.pkl. The package root istarget.pkl(Config + Auth only). Per-version subdirs each hold their ownk8s.pklcontaining the SubResource classes valid for that minor. Avoids the basename collision that would otherwise force<baseName>_<sanitizedVersion>aliases. shared.pklat the root. Version-agnostic typealiases (KubernetesMinorVersion, etc.) and theK8sVersionannotation class live here. Per-version files reference them through the package root, so the version dropped doesn’t strand orphan imports.- Per-version
PklProject. Eachv<X.Y>/ships its ownPklProjectso the per-version subtree can stand alone if needed (Pkl resolves@k8s/v1.34/...paths through that nested project). helm/is parallel. Helm-chart wrappers also ship per-version, in their own subtree, because they import per-version subresource files (@k8s/v1.34/k8s.pkl) and therefore need to be regenerated when versions change.
Add the apiVersion field to Config
schema/pkl/target.pkl
kubernetesVersion (matched by the case-insensitive ApiVersion / apiVersion lookup) and feeds it into resolveSchemaVersions. Users write:
Define the version annotation class
schema/pkl/shared.pkl
v<X.Y>/ subtree, fields that were added in a later minor are simply absent from the file. The annotation is what drives that selection at publish time, so when a consumer imports @k8s/v1.30/core/Service.pkl, they only see fields that were available in K8s 1.30. trafficDistribution (Beta in 1.30) is present, while tolerance (Beta in 1.35) is not.
Three granularities of gating, all visible in the per-version trees as additions or omissions:
Property-level, field landed in 1.30, beta then GA
Module-level, whole resource available from 1.29
Class-level, nested class gated independently
Resolve the runtime version (Go side)
Beyond the Config field, the plugin also resolves the runtime version (for client construction + preflight). Priority order:pkg/config/version.go
CheckField helper walks the per-version gate metadata and refuses to apply when the user pins a gated field on a cluster that doesn’t accept it. Catches the “I added trafficDistribution to my Service.pkl but my cluster is 1.29” mistake before any RPC is sent.
Author against versioned schemas
Users in theirforma.pkl import paths matching the version they’re targeting:
examples/nginx-v1.34.pkl
@k8s/v1.34/... paths are the per-version subtrees in the published package. The package root @k8s/target.pkl carries only Config + Auth (version-agnostic). When the Forma is extracted later, core’s ImportsGenerator narrowing makes sure only v1.34-relevant resources show up in the regenerated file.
For the full contract, the version-resolution priority order, and the extract-time narrowing that ties this together, see API schema versioning.
