Skip to main content
This guide walks through how 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 the schema/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.
Important properties of this layout:
  • No top-level k8s.pkl. The package root is target.pkl (Config + Auth only). Per-version subdirs each hold their own k8s.pkl containing the SubResource classes valid for that minor. Avoids the basename collision that would otherwise force <baseName>_<sanitizedVersion> aliases.
  • shared.pkl at the root. Version-agnostic typealiases (KubernetesMinorVersion, etc.) and the K8sVersion annotation class live here. Per-version files reference them through the package root, so the version dropped doesn’t strand orphan imports.
  • Per-version PklProject. Each v<X.Y>/ ships its own PklProject so 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.
The kubernetes plugin includes 16 minor versions today (v1.21 to v1.36). Each new minor adds one subdir; an unsupported minor would be dropped from this list. CI runs a conformance matrix that exercises every minor against a real kind cluster.

Add the apiVersion field to Config

schema/pkl/target.pkl
Core picks up 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
Inside each per-version 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
A preflight 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 their forma.pkl import paths matching the version they’re targeting:
examples/nginx-v1.34.pkl
The @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.