In Rego, how would you warn on every removed apiVersion anywhere in a repo's manifest bundle?
answer
- the depth is not known in advance
- the top level is not the only level
- report where, not just how many
- a set of messages, not a boolean
- strings hide manifests from the traversal
basics
~20 sWalk the whole bundle instead of enumerating paths: for every node that is an object whose apiVersion is in your removed set, emit a warning that includes the walk path. The path is what makes the advisory list actionable.
solid answer
~50 sThe trap is writing `input[_].apiVersion` — it only checks the top level of each document, and an `apiVersion` can sit lower down: inside a list-kind document's `items`, inside a custom resource that embeds a whole object, inside a rendered template. So use `walk(input, [path, value])`, filter to objects, test `value.apiVersion in removed_versions`, and build the message with `sprintf` so it carries both the version and the path — `path[0]` identifies which document in the bundle, the rest locates it inside that document. Emit into a `warn` set rather than a blocking rule, because a pre-upgrade sweep across every repo is an inventory exercise: the platform team wants a complete, deduplicated, sorted list of locations to hand to teams, not a wall of failed pipelines on changes that were fine yesterday. One caveat: `walk` will not descend into a manifest that is embedded as a *string*, so decode those first if the bundle contains any.
code
rego · 11 linespackage upgrade
removed_versions := {"extensions/v1beta1", "policy/v1beta1", "batch/v1beta1"}
warn contains msg if {
some path, value
walk(input, [path, value])
is_object(value)
value.apiVersion in removed_versions
msg := sprintf("removed apiVersion %v at %v", [value.apiVersion, path])
}go deeper
Recall the pairing: walk gives you a path and a value for every node, and the path is what lets a message say where the problem is.
Explain why a fixed-path rule under-reports here and why the miss is silent, then write the walk plus set-membership body with the message built by sprintf.
Show the operator's judgment: advisory output first, deduplicated and sorted, plus an honest statement of what the sweep does not cover.
Own the framing with other teams. A cross-repo sweep is an inventory you hand over with a timeline, not a surprise failure in someone else's pipeline on a Friday.
## The situation A cluster upgrade is coming and a set of API versions will stop being served. The platform team needs to know, across every repo, where those versions still appear, before anyone's deployment starts failing. This is a sweep, not a gate: the output is an advisory inventory with locations, and the enforcement conversation happens later. ## Why the obvious rule under-reports The first attempt is nearly always a fixed path: ``` warn contains msg if { some doc in input doc.apiVersion in removed_versions msg := sprintf("removed apiVersion %v", [doc.apiVersion]) } ``` That catches only the top level of each document. `apiVersion` legitimately appears deeper: a document whose `kind` is a list carries whole objects under `items`; a custom resource can embed a complete object under one of its own fields; generated bundles nest objects inside templated wrappers. Each of those is an occurrence that will break on upgrade and that this rule does not see. And it under-reports *silently* — a fixed-path reference that misses is undefined, and an undefined body just produces nothing. This is exactly the case `walk` exists for: the field can be at a depth you cannot enumerate ahead of time, across documents you did not write. ## The rule ``` removed_versions := {"extensions/v1beta1", "policy/v1beta1", "batch/v1beta1"} warn contains msg if { some path, value walk(input, [path, value]) is_object(value) value.apiVersion in removed_versions msg := sprintf("removed apiVersion %v at %v", [value.apiVersion, path]) } ``` Four things are doing work here: - **`walk` gives location as well as value.** Without `path`, the output is "this bundle contains three occurrences", which nobody can act on. With it, each message names the document index and the exact field chain. - **`is_object(value)`** narrows the stream. Strictly it is optional — referencing `.apiVersion` on a string or a number is undefined rather than an error, so those nodes drop out anyway — but it states the intent. - **The set membership** keeps the list of versions in one named place rather than scattered through the body, so extending the sweep for the next upgrade is a one-line change. - **`warn` as a partial set** deduplicates identical messages for free and keeps the output a collection rather than a single boolean. If you want a dotted location instead of the raw array, build one with a comprehension that stringifies each step first — `concat(".", [sprintf("%v", [step]) | some step in path])` — because array indexes are integers and `concat` needs strings. Sort the result if the output feeds a report you will diff between runs; the traversal order is not something to depend on. ## What the sweep still misses, and say so Be honest about the boundaries when you present the inventory, because a list that is presented as complete and is not is worse than no list: - **Embedded strings.** `walk` sees a manifest carried as a string field as one opaque scalar. If the bundle contains those, decode them with `yaml.unmarshal` or `json.unmarshal` first and walk the decoded value too. - **Unrendered templates.** Anything that is generated rather than committed has to be rendered before the engine sees it, or you are sweeping the template rather than the output. - **What is deployed versus what is committed.** A repo sweep reports what is in git. Objects in a cluster that no repo owns are a different search. ## Why warn rather than block A pre-upgrade sweep hits changes that were legitimate the day before the policy landed. Blocking turns an inventory exercise into a queue of blocked teams who did nothing wrong and have no fix ready, and the predictable result is a rush of exceptions that outlive the upgrade. A warning path produces a per-repo list with concrete locations, which is what a team needs to plan the work. The decision about when that becomes a hard failure is a separate, later conversation with its own timeline. ## What an interviewer is checking That you recognise the unknown-depth requirement immediately, that you use the `path` half of `walk`'s output rather than discarding it, that you can name where a fixed-path rule under-reports, and that you have the judgment to know a sweep across other teams' repos starts as an advisory list.
- Why include the path in the message rather than just the offending value?Because the consumer is another team that has to fix it. "Three removed apiVersions in this repo" starts a search; "removed apiVersion at [3, spec, template] in bundle document 3" starts an edit. The path's first element also tells you which document in a multi-document bundle you are in.
- A repo's bundle carries one manifest as a string field. Does your rule see it?No. walk treats a string as a leaf, so anything inside it is invisible and the sweep under-reports without saying so. Decode it with `yaml.unmarshal` or `json.unmarshal` and walk the decoded value as well, guarding the decode so an unparseable payload is reported rather than skipped.
- Would you have used walk if you only needed to check the top-level apiVersion of each document?No. When the path is known, a direct reference is clearer to the next reader and does not require shape guards. walk earns its place here specifically because the field appears at depths nobody can enumerate across other teams' repos.
- How do you make the output stable enough to diff between weekly runs?Collect into a set so identical messages deduplicate, then sort before reporting. Traversal order is not something to rely on, so an unsorted list can reshuffle between runs and make a diff look like change when nothing moved.
saying these in an interview costs you the question
- Only checks the top level of each document
- Emits a count or a boolean with no location
- Assumes walk descends into an embedded string manifest
- Blocks every repo on the first sweep
- Hardcodes the version list inline in several rules