platform.engineering/k8sofficialv0.1.12stableresource

Kubernetes resource plugin

installation
$formae plugin install k8s
changelog as of v0.1.13-dev.1 · structured per keep a changelog
v0.1.11stable
Added
  • AKS targets can name their cluster instead of describing it. endpoint and certificateAuthority are now optional on AKSAuth: give it a resource group and cluster name and the plugin reads both from Azure. Stating them still works and always wins, which is the path for a private endpoint or anything formae does not model. subscriptionId is new, falling back to the agent's AZURE_SUBSCRIPTION_ID, because a hosted agent can span subscriptions.

    AKS is the only cloud auth type that does this, and the reason is specific to Azure: the CA is not on the ManagedCluster resource at all. It is reachable only through the credentials endpoints, which the azure plugin calls during Read and which silently yields nothing when the call is denied or the cluster sets disableLocalAccounts. A discovered cluster then carries an empty CA with no indication why, and the failure surfaces later as a bare 401. Reading it at connect time makes that a real error naming the permission to grant. The lookup prefers ListClusterUserCredentials (AAD-based, needs only listClusterUserCredential/action, survives disableLocalAccounts) and falls back to admin; only the server URL and CA are taken from the returned kubeconfig, and the token still comes from the agent's own Azure identity.

    EKS, GKE, OVH and OKE are unchanged and still require both fields. Their endpoints and CAs are already on the resources formae discovers, so there is nothing to repair and no reason to spend an API call or a permission on it.

  • GKEAuth can name its cluster. The Pkl class exposed only endpoint and certificateAuthority, while GKEAuthConfig read ProjectId, Location and ClusterName and CacheKey composed all three. Nothing could set them, so the Go fields were dead and every GKE target keyed its token cache as GKE|<endpoint>|||, telling two clusters apart by network address alone. The three fields are now optional on GKEAuth, and examples/clusters/gcp.pkl sets them.

  • examples/connect-matrix/ — a cluster/connect file pair per cloud (AWS, GCP, Azure) that applies the cluster in one forma and the Kubernetes target in a separate one, which is how a hosted agent has to do it when the cluster already exists or was discovered. Every shipped example emits cluster and target together, so that path had no coverage. The README carries the per-cloud matrix, the three scenarios (formae creates it, it already exists, discover then adopt then connect) and what each cloud's discovery does and does not persist.

Changed
  • CI evaluates every forma under examples/ (scripts/eval-examples.sh, 34 files). The pkl-validate job only ever evaluated formae-plugin.pkl, so schema drift in a plugin dependency could break an example for months without a red check.
  • Every hasProviderDefault schema annotation now carries a recorded disposition in schema/provider-default-dispositions.json, enforced by a unit test: new annotations fail CI until classified, and rows for removed fields fail as stale. Object metadata labels and annotations are recorded as co-owned, because Kubernetes writes into both alongside the user; every other annotation starts as pending, and classifications land per field as the provider-default audit reaches them.
Fixed
  • A target config carrying an unresolved reference now fails by name. formae replaces every reference it resolves with the scalar value before a plugin sees it, and passes the ones it could not resolve through structurally intact with no $value. The plugin's ResolvedString read $value out of such an envelope, found nothing, and produced "" — an EKS token minted with an empty x-k8s-aws-id, an AKS token against an empty resource group, and a 401 from the API server naming none of it. The type is gone; every auth field is a plain string again, and FromTargetConfig rejects an Auth block still carrying a $res/$ref object, naming every offending field at once.

  • A cloud auth block missing a required identifier is rejected. An EKS config without ClusterName was accepted all the way through and signed with an empty x-k8s-aws-id. Every cloud auth type now checks the fields its provider cannot work without, matching what the Pkl schema already marks non-optional, and reports them together: EKS auth config missing required field(s): ClusterName.

  • The examples/lgtm-observability/ formae evaluate again. All five (local, aws, azure, gcp, oci) set username/password on the Grafana Target's Config, which the published grafana schema (grafana@0.1.3, the pinned dependency) has never defined — pkl eval failed with Cannot find property 'username' in object of type 'grafana#Config'. The Target now authenticates with the agent's GRAFANA_AUTH env var, which is what examples/lgtm-observability/README.md already documented. Sourcing the password from the managed lgtm-grafana-admin Secret needs a Config.auth block; that lands when a grafana schema carrying it is published to the hub.

  • A K8S::Custom::Resource no longer fails when its CRD arrives late in the same apply. The apply path already reset the RESTMapper and retried on no matches for kind, but the loop gave up after 30s — too short for a K8S::Helm::Release that installs CRDs alongside its controller (cert-manager's took ~66s to establish). The wait now defaults to 180s and is a plugin setting on the agent's formae.conf.pkl entry:

    agent {
      resourcePlugins {
        new k8s.PluginConfig { crdEstablishTimeoutSeconds = 300 }
      }
    }

    A cert-manager Release plus its ClusterIssuers no longer has to be split across two applies.

  • A live uninstall is no longer reported as abandoned. Delete started its Helm uninstall without registering it in the in-flight registry, and "a release record this plugin owns with no operation behind it" is exactly how an abandoned uninstall is recognised. So the first Status poll — 20 seconds after Delete under the default statusCheckInterval — declared every uninstall slower than that abandoned, with a recoverable error code that asks the agent to re-drive Delete, starting a second concurrent uninstall of the same release. Slower than 20s is ordinary: a pre-delete hook, or Wait=true sitting through a Pod's terminationGracePeriodSeconds.

    Only podinfo-sized charts escaped it, which is why the conformance destroy step passed throughout: its record is purged before the first poll. No test chart in the repo declares a delete hook, and the kratos scenarios call formae destroy from a trap EXIT cleanup that swallows failures.

  • A release whose objects never become ready now fails instead of polling forever. Under Wait=false Helm records deployed as soon as the apiserver accepts the manifests, and that record never changes again — so a Pod stuck in ImagePullBackOff from a typo'd tag, or one no node has room for, left Status answering InProgress for eternity. Nothing above caught it either: the agent fails an operation when a plugin goes silent, never because it keeps reporting progress, and there is no cap on how long an operation may run. The readiness wait is now bounded by the timeout recorded on the release, the same clock that already bounds a pending release, and the failure names the object that never came up. An operation this process is still running is exempt, so a slow hook is never cut short.

  • An uninstall is bounded by the release's own timeout, not the package default. A release given timeoutSeconds = 1800 had its uninstall cut off at 600s while the stalled-release verdict waited twice 1800s before saying so, leaving the command InProgress for the best part of an hour on work nothing was doing.

  • Upgrading a chart with subcharts no longer fails to render. Re-applying the deployed version reuses the chart stored in the release record instead of fetching it, but chart.Chart.dependencies is unexported and carries no JSON tag (helm/pkg/chart/chart.go:56), so Helm's own storage drops every subchart on the way in — while Metadata.Dependencies, which is serialized, goes on listing them. Rendering that remnant failed on any helper a dependency defines, which for an ory chart is the whole templates directory:

    template: no template "ory.extraEnvContainsEnvName" associated with template "gotpl"
    

    Such a chart is re-fetched now — but only when there is somewhere to fetch from. An adopted release has no repoURL, because Helm never records where a chart came from, so insisting on a fetch there would fail every upgrade of an adopted subchart chart outright. With nothing to fetch from, the incomplete stored chart is used anyway: rendering it may well succeed, and when a dropped subchart really is needed Helm names the template it cannot find. Trading a possible failure for a certain one is not an improvement.

    The no-op check that stops a re-driven Create re-running hooks is unaffected — it compares a version and a set of values and renders nothing, so an incomplete stored chart tells it nothing.

  • The plugin's own release labels no longer leak into resource state. formae.dev/managed — and now formae.dev/timeout-seconds — were reported back in metadata.labels, which put them into formae extract output and made them read as drift against a forma that never declared them.

Removed
  • HelmChart.pkl and its per-version wrapper trees are gone — 281 files, ~39k lines: schema/pkl-helm/ (the HelmChart module, 17 api-group mappers and the gen-versioned-helm codegen) and the generated schema/pkl/helm/v1.21…v1.36/ trees, plus the make targets generate-versioned-helm-schemas, verify-helm-schemas and the CI job that ran the latter.

    Breaking for anyone importing @k8s/helm/v<X.Y>/HelmChart.pkl. Migrate to K8S::Helm::Release: one resource per chart, Helm applies the objects. There is no mechanical rewrite — HelmChart produced N typed resources in formae state and Release produces one, so the release adopts what the chart already installed rather than inheriting per-object state.

    It was removed rather than deprecated because it could not honour hooks. helm template emits hook-annotated manifests with no orchestration, so a pre-install Job became a permanent resource that never re-ran, hook-weight was ignored, finished hooks accumulated, and test hooks were applied on every reconcile. Charts that relied on hooks applied silently wrong, which is a worse failure than not being supported.

    Removing it also drops the pkl-readers/helm@0.1.2 package dependency and the pkl-reader-helm external-reader declaration from the example projects: nothing renders a chart at Pkl-eval time any more.

    Deleted with it: the seven HelmChart-based single-file examples (nginx*.pkl, postgresql-v1.31.pkl, memcached-v1.31.pkl, create-namespace-test.pkl, imagepullsecrets-test.pkl) and two already-dead make targets — chart-test, whose script had been removed, and conformance-test-charts, whose *-chart filter matched no remaining fixture. examples/flux/flux-helm.pkl is ported to a Release, so Flux still installs with one formae apply.

Added
  • K8S::Helm::Release — Helm charts driven by the embedded Helm SDK. Formae manages the release; Helm manages the objects the chart renders. Hooks, hook weights, hook delete policies, CRD install ordering and revision history all work because Helm implements them, not because the plugin reimplements them. The release is a genuine Helm release, so helm list, helm history and helm rollback see it.

    Create/Update submit with Wait=false and return InProgress once Helm has written the release record; Status polls the record and then checks every rendered object with Helm's own ReadyChecker. The plugin stores nothing — all state lives in the release Secret and the cluster.

    A release is only recorded by formae once it is fully deployed. Create returns no NativeID, so a half-installed release is never put under management; Status supplies it after the release reaches deployed and every rendered object passes readiness. The RequestID carries namespace, name and target revision, and is what Status uses to find the release meanwhile. Update is exempt — the resource is already in state.

    Consequence to be aware of: a first install that fails leaves formae with no handle on the release, so formae destroy cannot clean it up. Re-applying the same forma does recover it — see the crash-recovery entry below.

    Complements HelmChart.pkl, which renders client-side and decomposes into typed resources. Prefer Release for charts with hooks, CRDs or subcharts; prefer HelmChart when per-object formae state matters more than chart fidelity.

  • A crash mid-install no longer needs manual recovery. Helm has no server-side operation controller: the install runs in the plugin process, so when that process dies the work dies with it, and Helm's pending-install status is left behind as a lock with no owner. Helm refuses both install and upgrade on a release in that state, and its documented way out is helm uninstall — which destroys the objects and re-runs pre-install hooks to get back to where it already was.

    The plugin now clears that lock itself on the next operation, doing what Helm's own failRelease does: set the status, write the record back. Whether it settles on deployed or failed is decided by the cluster, because Helm writes deployed last — after the hooks have run and every object exists:

    Releases.Create(pending-install) -> hooks -> create objects -> SetStatus(deployed)
    

    Dying anywhere in that middle stretch leaves an identical record, whether the work finished or never started. So recovery checks: every object the release renders present and ready means the install did complete and only the record was lost — it is recorded deployed and reported as success, with no second Helm operation and no hooks re-run. Anything missing means failed, which an upgrade is allowed to run over, three-way merging what is absent.

    The guarantee, whatever died:

    The command reaches a verdict It never sits in InProgress reporting work nothing is doing
    A failure says why e.g. objects are incomplete (ConfigMap/app-config is absent)
    The next apply converges No operator, no helm uninstall

    Two limitations worth knowing before you rely on this:

    • A plugin crash defers recovery to the next apply. The agent's PluginOperator runs on the plugin's node and dies with it, so the agent fails the command without calling Status again and the plugin never gets the chance to clear the record. The release stays pending-install until something applies again — which then recovers it automatically.
    • That failure currently carries no message, because it comes from the agent's own missing-in-action path rather than from the plugin.
Changed
  • K8S::Helm::Release moved to @k8s/helm/Release.pkl — one copy, not sixteen. The module shipped in every v<X.Y>/ tree as 16 byte-identical copies; no field on a Helm release has a shape that depends on the apiserver minor, so it now ships once at the package root.

    Breaking for the import path only:

    import "@k8s/v1.33/helm/Release.pkl" as helm   // before
    import "@k8s/helm/Release.pkl" as helm         // after

    Nothing else changes — same type, same fields, same state. Every other schema import keeps its v<X.Y>/ segment.

    This needs formae >= 0.89.0. The hoist was tried and reverted once because formae extract globbed only @k8s/*.pkl plus @k8s/v<ver>/**/*.pkl, so a resource module in a root-level subdirectory was invisible to it and extract died with Cannot find key "K8S::Helm::Release". formae#584 widened the glob to cover version-independent subtrees, which is what makes this possible.

    tools/gen-versioned-reflect grew a versionIndependentDirs set for this and rejects a @K8sVersion gate inside one: with no per-version copy left there is nothing to filter, and the gate would otherwise be silently ignored.

v0.1.10stable
Changed
  • A custom resource's spec now updates as one whole value. Changing any part of the spec sends the complete document to the apiserver in a single replacement, the way kubectl apply of the full manifest behaves, instead of a series of per-field edits formae computed from a document whose grammar it cannot know. minFormaeVersion is raised to 0.89.0 accordingly.
Fixed
  • Empty objects and lists in a custom resource spec reach the cluster exactly as written. For many custom resources an empty member is itself the configuration: a cert-manager ClusterIssuer selects the selfSigned issuer type with selfSigned = new Dynamic {}. Previously formae cleaned empty objects and lists out of the spec before writing, so the apiserver received an empty spec and admission webhooks rejected it, and adding a placeholder inside the empty member did not help because the cleanup collapsed it again. The spec is now preserved byte for byte. A custom resource that already lost empty members this way is repaired by the next apply, and a placeholder value added to work around the old behavior can be removed.

  • Paused Deployments settle instead of polling forever. A Deployment with spec.paused: true never converges its replica counts — the controller stops reconciling by design — so Status() reported InProgress until the operation timed out. A paused Deployment is now Success once the apiserver has observed the paused spec.

  • StatefulSet OnDelete and partitioned rollouts settle. With updateStrategy.type: OnDelete, pods are only replaced when deleted by hand, so status.updatedReplicas never advances and the rollout looked stuck forever. OnDelete now gates on readiness of the desired set only. With rollingUpdate.partition: N, only ordinals >= N are updated, so the reachable updated count is replicas - partition (floored at 0) rather than replicas.

  • DaemonSet OnDelete rollouts settle. Same root cause as the StatefulSet case: status.updatedNumberScheduled never reaches desiredNumberScheduled under OnDelete, so status now gates on numberReady alone.

  • No more false drift against an HPA. When a HorizontalPodAutoscaler scales a Deployment, ReplicaSet, or StatefulSet, the HPA — not formae — owns spec.replicas. formae still read the live count back and reported it as drift against a forma that deliberately omitted replicas, every reconcile. The plugin now consults metadata.managedFields and strips spec.replicas from reported state whenever the formae field manager does not own it. A forma that does declare replicas is unaffected — formae owns the field and it keeps drifting as before, which is the intended behavior for that case.

  • Over-marked createOnly fields no longer force a destructive replace. Every createOnly field makes formae plan a delete-then-create replace when the value changes. Five fields were marked immutable but are in fact accepted in place by the apiserver, so formae was destroying resources for changes Kubernetes would have taken: CSIDriver.spec.requiresRepublish, CSIDriver.spec.tokenRequests (both mutable since Kubernetes 1.22), PriorityClass.globalDefault, RuntimeClass.overhead, and RuntimeClass.scheduling. Each verdict was verified against a live apiserver. The fields that are genuinely immutable keep createOnly, and their per-field docstrings say so.

Changed
  • Rollout progress is visible while an operation runs. The plugin blanked StatusMessage on every non-Failure result, so the per-resource reason row stayed empty during a rollout. Provisioner messages now pass through on InProgress (e.g. replicas: 2/3 ready) and are blanked only on terminal Success, where a lingering message is just noise.
  • Drop the removed --watch flag from the example commands in the README, CONTRIBUTING, the helm/flux/crossplane/bookstore/custom-resource docs, and the example file headers. formae apply/destroy are submit-then-poll.
Added
  • K8S::Core::Secret adopts formae's first-class map-shaped secret types. A Secret's value can now be referenced one key at a time with secret.res.secretValue.at("key"), resolved live at the plugin-call boundary so a consumer such as a target credential picks it up without an agent restart. The decoded value is hashed at rest and excluded from drift detection. Requires formae 0.89.0 or later; minFormaeVersion is raised to 0.89.0.
  • examples/rollout-safety/ — one folder per case (paused Deployment, OnDelete StatefulSet, partitioned StatefulSet, HPA coexistence), each with create.pkl/update.pkl and the old-vs-new plugin behavior in the header.
v0.1.9stable
Changed
  • Bump examples to the latest formae 0.88.0 schema.
v0.1.8stable
Fixed
  • Unreachable clusters are now reaped. A resource Read against an unreachable apiserver (connection refused, DNS failure, dial/read timeout) fell through to a raw error the host rendered as UnforeseenError, which carries no health signal — so the target reaper never saw the cluster as unreachable and never reaped it. The Read funnel now maps a genuine client-side transport failure to NetworkFailure/ServiceTimeout. Auth/credential failures are deliberately excluded: a bad token surfaces as a *url.Error that satisfies net.Error even though the apiserver is reachable, so classification unwraps to concrete net types and skips the auth path — a healthy cluster is never reaped over a bad credential.
Changed
  • client-go bumped to v0.36.3 (k8s.io/api and k8s.io/apimachinery moved in lockstep), staying pinned to the highest supported K8s minor (1.36).
  • Builds on Go 1.26. Workflow go-version pins now match the go 1.26.0 declared in go.mod.
  • Examples pinned to formae 0.88.0. The runtime requirement is unchanged — minFormaeVersion remains 0.86.0.
v0.1.7stable
Changed
  • Breaking: pod and job template metadata no longer carries name/namespace. The metadata of pod and job templates (spec.template.metadata on Deployment, StatefulSet, DaemonSet, ReplicaSet and Job, and spec.jobTemplate.metadata on CronJob) changes type from ObjectMeta to PodTemplateMetadata, which holds only labels and annotations. Kubernetes accepts name/namespace on templates but never uses them (pods get generated names), so the schema previously forced you to invent a value with no effect. Formas that construct the metadata class explicitly inside a template need a one-line migration: swap new k8s.ObjectMeta { name = "..."; labels { ... } } for new k8s.PodTemplateMetadata { labels { ... } } and drop the name. Because the cluster stored the template name, the first apply after upgrading that includes the pod template removes that stored field, changing the template hash and performing one rolling update of the workload, so plan for it as you would any rolling restart. Unaffected: the amend style (metadata { labels { ... } }), top-level resource metadata (its name is still required), and StatefulSet volumeClaimTemplates metadata.
Fixed
  • Discovery no longer skips workloads created outside formae. Discovery validated foreign manifests against the authoring schema, which required spec.template.metadata.name, a field Kubernetes ignores and virtually no Helm chart or kubectl-applied manifest sets. As a result most Deployments, StatefulSets and CronJobs created outside formae were silently missing from discovery: no error surfaced, the resources simply never appeared in the inventory. With the template-metadata change above, these workloads are discovered and can be brought under management.
v0.1.6stable
Added
  • Custom resources (CRD instances). The plugin can now manage any custom resource (instances of CRDs like cert-manager Certificate, Argo Application, Flux GitRepository, and so on) with no per-CRD Go code or generated schema, through a generic catch-all type. 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, with no kubectl bootstrap. 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.
  • 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.
Fixed
  • Helm: workloads ordered after their ServiceAccount. Helm-rendered workloads set serviceAccountName as a plain string, so formae applied Deployments concurrently with their ServiceAccounts, and 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. 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.
v0.1.4stable
Added
  • 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 down to 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. 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.
Changed
  • Supported version window (clarification). Two version ranges are in play, and they are not the same. Schema trees ship for 1.21 to 1.36 (16 minors), giving typed Pkl authoring and pkl eval-time field validation for that whole range. The runtime support window is 1.31 to 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 to 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 to 1.36, in lockstep with the pinned client-go). Targeting a cluster below 1.31 is not supported.
v0.1.3stable
Added
  • K8s 1.35 and 1.36 supported. The schema package now ships per-version subtrees for K8s minors 1.21 to 1.36 (16 minors). Target a specific cluster's API version through the kubernetesVersion field on the K8s Config. 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.
Changed
  • Schema layout. The published schema/pkl/ tree now splits responsibility cleanly between the package root and the per-version subtrees. target.pkl is the version-agnostic package root, carrying 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.
Fixed
  • 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.
v0.1.2stable
Added
  • 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.
type
resource
category
containers
license
FSL-1.1-ALv2
originator
platform.engineering
namespace
K8S
latest
v0.1.12 · stable
platforms
repo
github.com/platform-engineering-labs/formae-plugin-kubernetes
versions (18)
select a version to jump to its notes
v0.1.13-dev.1dev
2026-09-19
v0.1.13-dev.0dev
2026-09-18
v0.1.12stable
2026-09-15
v0.1.11stable
2026-09-07
v0.1.10stable
2026-08-30
v0.1.10-dev.1dev
2026-08-21
v0.1.10-dev.0dev
2026-08-11
v0.1.11-dev.0devyanked
2026-08-11
·yanked by — · 1mo ago· reason: wrong version
v0.1.9stable
2026-07-27
v0.1.8stable
2026-07-24
v0.1.7stable
2026-07-16
v0.1.6stable
2026-06-25
v0.1.6-devdev
2026-06-24
v0.1.5stable
2026-06-12
v0.1.4stable
2026-06-12
v0.1.3stable
2026-06-05
v0.1.2stable
2026-05-13
v0.1.1stableyanked
2026-05-13
·yanked by — · 4mo ago