Skip to content

fix(patch,resource-update): empty values survive hinted fields and unresolved reference adds plan - #705

Open
JeroenSoeters wants to merge 11 commits into
mainfrom
fix/empty-value-fidelity
Open

fix(patch,resource-update): empty values survive hinted fields and unresolved reference adds plan#705
JeroenSoeters wants to merge 11 commits into
mainfrom
fix/empty-value-fidelity

Conversation

@JeroenSoeters

Copy link
Copy Markdown
Collaborator

Summary

Two defects with one representational root cause: the property pipeline treats empty values as rendering noise and filters them, destroying values that are deliberate.

  • An opaque document field (e.g. a Kubernetes custom resource spec where selfSigned: {} IS the declaration) reached providers with its empty members recursively stripped, from the create payload, the diff inputs, and patch op values - valid objects were rejected by admission and the recursive collapse defeated workarounds.
  • Adding a NEW reference field to an existing resource planned nothing when the reference could not resolve at plan time: the unresolved envelope flattens to an empty string, the top-level empty-value filter drops it as noise, and the whole update is silently discarded before execution-time resolution can run.

Mechanism

  • A new opt-in schema hint, preserveEmptyValues, marks a top-level field's subtree as carrying meaningful empty collections. Every empty-collection normalization skips such subtrees: both diff inputs, post-diff op values, the spurious-add filter, and the plugin-bound property conversion (all contexts: write, Read, sync, delete). The persist merge keeps empty objects and arrays under such roots. Matching is by first RFC 6901 pointer segment against a root set derived from schema hints; dotted (nested subresource) hints grant nothing. The absence-scoped provider-echo tolerance is unchanged, empty strings gain no exemption, and Atomic keeps exactly its existing diff-granularity meaning - existing plugin schemas see no behavior change, pinned by tests.
  • The top-level empty-value drop's keep-set widens to fields whose desired pre-flatten value is a well-formed reference envelope (strict predicate: a $ref with a parseable identity, or a complete $res declaration). A first-declared unresolvable reference now plans a placeholder op that execution resolves live; only the placeholder ever persists. CreateOnly destinations are excluded: a replacement is never planned from a value that has not resolved.

Tests

Unit pins for every touched site (fidelity at each strip, defaults preserved for unhinted fields, path matcher prefix/escape cases, envelope predicate matrix, churn contract, no-change guarantees) and workflow acceptance: an opaque-bodied resource creates verbatim, re-applies clean, and updates as one whole value still carrying its empty member; an existing resource gaining a reference to a hashed secret plans, delivers the resolved value together with an ordinary sibling change, re-applies clean, and persists no plaintext.

A follow-up in the kubernetes plugin opts K8S::Custom::Resource.spec in with updateMethod = "Atomic" + preserveEmptyValues = true and a minFormaeVersion bump.

An opt-in FieldHint marking a top-level field's subtree as carrying
meaningful empty collections: normalization must preserve them rather
than strip them as rendering noise. Orthogonal to UpdateMethod.
…survive empty-value normalization

Fields hinted preserveEmptyValues keep their empty collections through
both diff inputs, op values, and the spurious-add filter, matched by
first pointer segment against a root set derived from schema hints. The
top-level empty-value drop's keep-set widens to fields whose desired
pre-flatten value is a well-formed reference envelope, so a first-declared
unresolvable reference plans a placeholder add that execution resolves
instead of being silently discarded. The absence-scoped provider-echo
tolerance is deliberately unchanged for preserved roots.
A kept placeholder on a createOnly destination could only surface as a
replacement, and a replacement must never be planned from a value that
has not resolved: an import-shaped source whose property is never written
would otherwise destroy its consumer. Placeholder adds are minted for
mutable destinations only.
…eptance

A preserveEmptyValues-hinted field reaches the plugin verbatim on create,
re-applies clean, and updates as one whole value; the persist merge keeps
its empty collections when the plugin echoes nothing. An existing resource
gaining a reference to a hashed secret plans, delivers the resolved value
together with an ordinary sibling change, re-applies clean, and persists
only the placeholder at rest.
The hint's invariant covers empty collections, which pass the top-level
empty-value filter on their own; granting preserved roots a keep-set
entry only changed empty-string behavior, minting spurious adds for
fields an unset nullable declaration renders as an empty string.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant