diff --git a/CHANGELOG.md b/CHANGELOG.md index 37a1763e..3ba26ab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,33 @@ 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. + +### 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 @@ -52,21 +75,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 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: