hardening(parser): extends drops child deploy/cluster + non-presence-aware clear (slice of #209)
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 7
- Forks
- 3
- Avg merge
- 2h 12m
- Merged PRs (30d)
- 30
Description
Carved out of #209 ("definition-drift safety") so the extends merge bug can be fixed in isolation, smallest-first. The rest of #209 (snapshot-vs-current def, admission_fingerprint fence, revival-set validation, native-takeover fence, drifted-gate reconciliation) stays there as separate steps and is not in scope here.
Problem
mergeJobDef (server/pkg/parser/extends.go:97) starts out := base and then overrides field-by-field. Three job fields are handled incorrectly:
DeployandClusterhave NO merge branch — sooutkeepsbase.Deploy/base.Clusterand a child's owndeploy:/cluster:is silently dropped. A child that extends a base and declares its owndeploy:loses it; a child that setscluster:to override the template's is ignored.Clusteris a plainstring(JobDef.Cluster, checked!= ""), socluster: null,cluster: ""and an absent key all collapse to""— there is no way to clear an inherited cluster. Combined with the missing merge branch,cluster: nullon a child silently inherits the template's (possibly prod) cluster instead of clearing it. This is a footgun: it can point a job at the wrong cluster.Deployis*DeploySpec(nil = absent), sodeploy: nullcollapses to nil and cannot be distinguished from "absent" — no explicit clear.Environment(#206) is currently child-wins-or-zero (extends.go:171-178:out.Environment = yaml.Node{}, then the child's if present) — it deliberately does NOT inherit from the base. The #206 comment states this was left for #209: "base inheritance is deliberately NOT added (that's #209, with deploy:/cluster:)."
Net effect: extends is unusable for sharing a deploy config, and cluster: null is a silent footgun.
Contract to settle (the key design decision)
Presence-aware three-state semantics for Deploy / Cluster / Environment:
| state | Option A — inherit (recommended) | Option B — safe (today's env behaviour) |
|---|---|---|
| absent | inherit base | zero (no inherit) |
| explicit value | override | override |
null / "" |
clear (never inherit) | zero |
Both options agree: explicit child value overrides, and null/"" clears (this is what kills the cluster: null footgun). They differ only on absent.
Recommendation: Option A, uniform for all three. It is what extends already means for every other field (image:, script:), a base sharing deploy:/cluster: is legitimate DRY, and the removal of the temporary env-non-inherit is exactly what #206 deferred to here.
Security note (must be decided consciously): Option A makes a base template's deploy:/environment: inheritable, so freeze governance (#202/#206) then applies to children that inherit a governed environment. That is correct when the inheritance is intended, and it is surfaced rather than silent (extending the base is an explicit choice; null clears). If the preferred posture is that deploy/environment must never inherit, choose Option B, or a hybrid (cluster inherits like other config; deploy/environment do not). Whichever is chosen, the presence-aware clear and explicit-override halves ship regardless.
Proposed direction
- Make
Clusterpresence-aware — ayaml.Node(mirroringEnvironment) or*string+ presence — so absent /null/ value are distinct and an explicit clear is possible. - Make
Deploydistinguishdeploy: null(explicit clear) from absent (a presence flag alongside the*DeploySpec, or a node wrapper). Ifdeploy: nullclear is judged unnecessary, state that explicitly and keep nil = absent. - Rewrite the
Deploy/Cluster/Environmentblock ofmergeJobDefto the agreed contract, uniform and presence-aware. - Presence-aware
JobDef.UnmarshalYAMLrecording presence/tag for these fields, preservingKnownFields(strict unknown-key detection must still fire). - Post-merge validation (runs AFTER the merge, so a child that clears or overrides is checked, not the pre-merge shape):
deploy×environmentcoherence,clusteronly meaningful with adeploy/target, and the existing invalid-cluster-name check (parse_job.go:26). - Emit round-trip (
server/pkg/parser/emit.go):Environmentalready round-trips (:175-178);Cluster/Deploymust round-trip presence (absent stays absent, explicit clear stays clear, value stays value).
Tests
- extends chain (grandparent → base → child) inheriting / overriding / clearing each of
deploy/cluster/environment; - explicit clear:
cluster: null(anddeploy: nullif supported) on a child extending a base that sets them → cleared, NOT inherited; - child explicit value wins; absent behaves per the chosen contract;
- round-trip (parse → emit → parse) preserves absent vs explicit-clear vs value;
- freeze governance under Option A: a child inheriting a base's governed
environmentis held by a freeze on that env (guards the security-sensitive path).
Baseline: origin/main (v0.83.0). Verify line numbers against current origin/main.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at mergeJobDef in server/pkg/parser/extends.go:97, then inspect JobDef.UnmarshalYAML, server/pkg/parser/emit.go, and parse_job.go:26. First settle the absent/value/clear contract for Deploy, Cluster, and Environment. Done means post-merge validation, round-trip preservation, and the listed inheritance, clearing, override, and freeze-governance tests pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, yaml
- Domain
- backend, ci-cd
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100