From 71bf56663bc5024ba52790859c4f324251344e32 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Mon, 10 Aug 2026 19:26:18 -0700 Subject: [PATCH 1/4] docs(changelog): note map-shaped secret adoption and drop the premature 0.1.10 heading K8S::Core::Secret adopts first-class map-shaped secrets: reference a single key with secret.res.secretValue.at("key"), resolved live, decoded value hashed at rest and drift-excluded. The 0.1.10 heading was added to the changelog but never tagged or published (the Hub is at 0.1.9), so its docs-only entry is folded back into Unreleased; everything here ships as the next release. --- CHANGELOG.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37a1763e..9013eaa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,21 +52,23 @@ formae agent. 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. -## [0.1.10] - -### Changed - -- 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. - ## [0.1.9] ### Changed From 1e47baab63b230843c13a08272380d23ac4c85b5 Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Sat, 29 Aug 2026 21:56:26 -0700 Subject: [PATCH 2/4] docs(changelog): custom resource spec fidelity and whole-value updates --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9013eaa0..83d82ee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,31 @@ formae agent. ## [Unreleased] +Requires formae >= 0.89.0 for the custom resource spec fidelity below; on an +older agent the spec keeps today's behavior. + +### 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 From 69a9e28d9b213b6f7d80071b28b7d84bc19b076a Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Sun, 30 Aug 2026 13:56:54 -0700 Subject: [PATCH 3/4] feat(custom): custom resource spec diffs atomically and keeps empty values A custom resource's spec is an opaque Any body: formae has no schema knowledge inside it, so whole-value replacement is the only update operation it can stand behind, and empty objects and lists inside it are configuration rather than rendering noise. Many CRDs select a variant by key presence alone: cert-manager's selfSigned: {} is a complete, valid issuer configuration. Previously formae's property pipeline cleaned empty collections out of the spec before writing, so the apiserver received an empty spec and admission webhooks rejected it, and a placeholder inside the empty member did not survive either because the cleanup collapsed it recursively. Hint spec with updateMethod Atomic and preserveEmptyValues true in the master schema and every versioned tree, and build against pkg/model v0.1.27, which carries the new FieldHint.PreserveEmptyValues field. formae core 0.89.0 and later honors the hint end to end; minFormaeVersion is already 0.89.0 on this branch, which closes the mixed-version window where an older agent would half-interpret the hint as atomic diffing without fidelity. A custom resource that already lost empty members to the old behavior is repaired by the next apply. --- go.mod | 2 +- go.sum | 4 ++-- schema/pkl-main/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.21/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.22/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.23/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.24/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.25/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.26/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.27/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.28/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.29/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.30/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.31/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.32/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.33/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.34/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.35/custom/CustomResource.pkl | 6 +++++- schema/pkl/v1.36/custom/CustomResource.pkl | 6 +++++- 19 files changed, 88 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 1c912eff..6ee14696 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.32.31 github.com/oracle/oci-go-sdk/v65 v65.122.0 github.com/ovh/go-ovh v1.9.0 - github.com/platform-engineering-labs/formae/pkg/model v0.1.26 + github.com/platform-engineering-labs/formae/pkg/model v0.1.27 github.com/platform-engineering-labs/formae/pkg/plugin v0.4.1 github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.6 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index f94523e4..01a05eeb 100644 --- a/go.sum +++ b/go.sum @@ -167,8 +167,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1 h1:ZMTgKwSomy2cVcl/+NivSqopbWeHbmYeQ+BxoYq8bVY= github.com/platform-engineering-labs/formae/pkg/api/model v0.1.1/go.mod h1:0ncHFCsGA6b0w1kBm6m+QwJ823qAY2vL47GvoR0BTyU= -github.com/platform-engineering-labs/formae/pkg/model v0.1.26 h1:80p843bmz9sLTtUFveMGWYeGcpRjvceyVxUgPvlApqs= -github.com/platform-engineering-labs/formae/pkg/model v0.1.26/go.mod h1:1dmsFwoaJZkHevBsAIZr068CWzG7de1VNz7PFWvM3Z8= +github.com/platform-engineering-labs/formae/pkg/model v0.1.27 h1:9EhTn19+iajQecS4/spXmtOEzoolw2/tO7TUiEoc/1o= +github.com/platform-engineering-labs/formae/pkg/model v0.1.27/go.mod h1:1dmsFwoaJZkHevBsAIZr068CWzG7de1VNz7PFWvM3Z8= github.com/platform-engineering-labs/formae/pkg/plugin v0.4.1 h1:dg8TBQVJR8DVF28bvmzAG4Ms1Id0yIr6Kd8c3UTO3iw= github.com/platform-engineering-labs/formae/pkg/plugin v0.4.1/go.mod h1:ZFXMfeZljHVDWQTSeJ4dj2EG3iTjdrxQjm7DpvOYUXk= github.com/platform-engineering-labs/formae/pkg/plugin-conformance-tests v0.2.6 h1:xslGa2DOFslxD0jxwI/XaUxdetpqSn++rfpqbW/uLbE= diff --git a/schema/pkl-main/custom/CustomResource.pkl b/schema/pkl-main/custom/CustomResource.pkl index 28fd7222..06c72bca 100644 --- a/schema/pkl-main/custom/CustomResource.pkl +++ b/schema/pkl-main/custom/CustomResource.pkl @@ -66,7 +66,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.21/custom/CustomResource.pkl b/schema/pkl/v1.21/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.21/custom/CustomResource.pkl +++ b/schema/pkl/v1.21/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.22/custom/CustomResource.pkl b/schema/pkl/v1.22/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.22/custom/CustomResource.pkl +++ b/schema/pkl/v1.22/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.23/custom/CustomResource.pkl b/schema/pkl/v1.23/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.23/custom/CustomResource.pkl +++ b/schema/pkl/v1.23/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.24/custom/CustomResource.pkl b/schema/pkl/v1.24/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.24/custom/CustomResource.pkl +++ b/schema/pkl/v1.24/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.25/custom/CustomResource.pkl b/schema/pkl/v1.25/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.25/custom/CustomResource.pkl +++ b/schema/pkl/v1.25/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.26/custom/CustomResource.pkl b/schema/pkl/v1.26/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.26/custom/CustomResource.pkl +++ b/schema/pkl/v1.26/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.27/custom/CustomResource.pkl b/schema/pkl/v1.27/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.27/custom/CustomResource.pkl +++ b/schema/pkl/v1.27/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.28/custom/CustomResource.pkl b/schema/pkl/v1.28/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.28/custom/CustomResource.pkl +++ b/schema/pkl/v1.28/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.29/custom/CustomResource.pkl b/schema/pkl/v1.29/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.29/custom/CustomResource.pkl +++ b/schema/pkl/v1.29/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.30/custom/CustomResource.pkl b/schema/pkl/v1.30/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.30/custom/CustomResource.pkl +++ b/schema/pkl/v1.30/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.31/custom/CustomResource.pkl b/schema/pkl/v1.31/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.31/custom/CustomResource.pkl +++ b/schema/pkl/v1.31/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.32/custom/CustomResource.pkl b/schema/pkl/v1.32/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.32/custom/CustomResource.pkl +++ b/schema/pkl/v1.32/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.33/custom/CustomResource.pkl b/schema/pkl/v1.33/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.33/custom/CustomResource.pkl +++ b/schema/pkl/v1.33/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.34/custom/CustomResource.pkl b/schema/pkl/v1.34/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.34/custom/CustomResource.pkl +++ b/schema/pkl/v1.34/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.35/custom/CustomResource.pkl b/schema/pkl/v1.35/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.35/custom/CustomResource.pkl +++ b/schema/pkl/v1.35/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: diff --git a/schema/pkl/v1.36/custom/CustomResource.pkl b/schema/pkl/v1.36/custom/CustomResource.pkl index a6d395f0..e59aa9ef 100644 --- a/schema/pkl/v1.36/custom/CustomResource.pkl +++ b/schema/pkl/v1.36/custom/CustomResource.pkl @@ -71,7 +71,11 @@ open class CustomResource extends k8s.Resource { /// is intentionally untyped (escape-hatch model — no per-field validation). /// `Any` (not just `Dynamic`) so it accepts both hand-authored Dynamic /// literals and typed objects produced by Helm chart rendering. - @k8s.FieldHint {} + /// Atomic: formae has no schema knowledge inside an `Any` body, so + /// whole-value replacement is the only update it can stand behind. + /// preserveEmptyValues: empty members are configuration here (an empty + /// object selects a variant by key presence), never rendering noise. + @k8s.FieldHint { updateMethod = "Atomic"; preserveEmptyValues = true } spec: Any? /// Composite identity, byte-identical to prov.CustomResourceID in Go: From f2bab2a7e353a2931e1636f73ca9a7c84600037d Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Sun, 30 Aug 2026 13:59:15 -0700 Subject: [PATCH 4/4] docs(changelog): release 0.1.10 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d82ee3..3ba26ab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Install with `sudo formae plugin install k8s` on the host that runs the formae agent. -## [Unreleased] +## [0.1.10] Requires formae >= 0.89.0 for the custom resource spec fidelity below; on an older agent the spec keeps today's behavior.