feat(hook): order manifest keys to match hand-authored convention - #19
Closed
neilverc wants to merge 1 commit into
Closed
feat(hook): order manifest keys to match hand-authored convention#19neilverc wants to merge 1 commit into
neilverc wants to merge 1 commit into
Conversation
ctx.std.yaml.stringify() is the sole YAML serialization path every kind's
hooks funnel through (parse -> mutate -> stringify), and its output is
written to disk byte-for-byte with no further re-marshal. Until now it
went through plain yaml.v3 map[string]any encoding, which always
alphabetizes keys — e.g. "containers" sorts ahead of "initContainers"
even though init containers run first.
Add orderedYAMLMarshal: builds a *yaml.Node tree by hand (preserving
explicit key order, unlike map[string]any) and applies keyOrderGroups on
top of the default alphabetical sort. Each group is an ordered list of
keys; applyOrderGroup gathers whichever of a group's keys are present
into a contiguous block, in the group's order, and splices that block in
at the position where the earliest one already sits — every other key,
including a group matched by only one key, keeps its current position
untouched. Groups are path-agnostic: they fire on sibling key names
wherever they co-occur, so e.g. the PodSpec group covers a Deployment's
spec.template.spec, a CronJob's nested spec.jobTemplate.spec.template.spec,
a bare Job, DaemonSet, StatefulSet, etc. with no per-kind knowledge.
Groups and their order are chosen to minimize reordering churn against
the ~450 hand-maintained services/*/_infra/*.yaml files in the api
monorepo (measured via a structural YAML walk classified by shape), not
to reproduce the Kubernetes API's declared struct order — the two
disagree about as often as they agree, and several evidence-backed
candidates (Container's name/image/etc, DeploymentSpec, CronJobSpec,
Probe) were tested and dropped because every ordering of their keys
still cost 54-100% churn: those keys are routinely interleaved with
untouched sibling fields in real files, with no consistent convention to
anchor a fixed order against, so any fixed order disturbs most of them
regardless. The five groups kept all measured under ~15% churn (except
ObjectMeta's pod-template case, a genuine ~68/32 split with no fully
resolving order, and JobSpec, kept anyway for its tiny blast radius):
- PodSpec: initContainers before containers (~17% churn). volumes is
declared right before them in k8s.io/api's struct order but sits far
from them in real files, so including it was dropped (99% churn).
- ObjectMeta: annotations, labels, name, namespace — one list serves
both the top-level metadata convention (labels-first, ~5%) and the
pod-template metadata convention (annotations-first, ~32%) since the
two contexts have almost disjoint typical keysets.
- VolumeMount: mountPath, name, readOnly, subPath (~1%; the declared
name-first order costs ~100%).
- ResourceRequirements: limits, requests, claims (~1%; already agrees
with the declared order).
- JobSpec (CronJob's embedded job template): parallelism, completions,
selector, template (~11%, 36 instances total).
neilverc
marked this pull request as ready for review
August 14, 2026 15:47
Collaborator
Author
|
can push inside api repo directly |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
ctx.std.yaml.stringify()is the sole YAML serialization path every kind's hooks funnel through (parse→ mutate →stringify), and its output is written to disk byte-for-byte with no further re-marshal. Until now it went through plainyaml.v3map[string]anyencoding, which always alphabetizes keys — e.g.containerssorts ahead ofinitContainerseven though init containers run first.What
orderedYAMLMarshalbuilds a*yaml.Nodetree by hand (preserving explicit key order, unlikemap[string]any) and applieskeyOrderGroupson top of the default alphabetical sort.applyOrderGroupgathers whichever of a group's keys are present into a contiguous block, in the group's order, and splices that block in at the position where the earliest one already sits — every other key, including one matched by only a single key, keeps its current position untouched.spec.template.spec, a CronJob's nestedspec.jobTemplate.spec.template.spec, a bare Job, DaemonSet, StatefulSet, etc. with no per-kind knowledge.How the groups were chosen
Groups and their order are chosen to minimize reordering churn against the ~450 hand-maintained
services/*/_infra/*.yamlfiles in theapimonorepo (measured via a structural YAML walk classified by shape), not to reproduce the Kubernetes API's declared struct order — the two disagree about as often as they agree. Several evidence-backed candidates (Container'sname/image/etc,DeploymentSpec,CronJobSpec,Probe) were tested and dropped because every ordering of their keys still cost 54-100% churn: those keys are routinely interleaved with untouched sibling fields in real files, with no consistent convention to anchor a fixed order against.The five groups kept all measured under ~15% churn, except
ObjectMeta's pod-template case (a genuine ~68/32 split with no fully resolving order) andJobSpec(kept anyway — tiny blast radius):initContainers, containersannotations, labels, name, namespacemountPath, name, readOnly, subPathlimits, requests, claimsparallelism, completions, selector, templatevolumeswas deliberately left out of the PodSpec group: it's declared right beforeinitContainers/containersink8s.io/api's struct order, but in practice sits far from them (nearrestartPolicy/serviceAccountName) in real files, so including it dragged the whole block across everything in between on nearly every file (99% churn either way).Validation
go build ./...,go vet ./...,gofmt -l .clean.go test ./...passes across every package, including new unit tests forapplyOrderGroup(no-op on zero/one match, splice-in-place semantics, doesn't drag a distant match forward) and integration tests throughctx.std.yaml.stringify()covering each kept group plus path-agnostic nesting (CronJob's pod spec).