feat(preview): add a one-month catch-all to the reaper #64
No reviewers
Labels
No labels
blocked
bug
duplicate
effort
high
effort
low
effort
medium
effort
minimal
enhancement
help wanted
invalid
pr
feedback
pr
reviewing
question
status
backlog
status
document
status
done
status
implement
status
pending
status
planning
status
reviewing
waiting
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
tepichord/milner!64
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/reaper-collects-any-preview"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The reaper's existing pass is untouched: a namespace selected by
milner.dev/preview-prwhosemilner.dev/preview-expireshas passed iscollected, and every object is deleted by that label. A non-numeric or missing
expiry is still skipped. That pass is byte-identical to
mainapart from asection comment.
What it could never see is a preview with no label. CI runs the branch's
pipeline, so a pull request that never rebased onto the split created its
namespace with the step that predates the label, and no step revisits a
namespace that already exists to add it. L3's create-only rule (the
preview-deployerSA holds create and delete, not patch) meant there was nobackfill path either. Nothing then moved such a namespace towards collection:
preview-61sat past its four-hour stamp until someone looked, and elevenbranches still carry the pipeline that mints more like it.
So a second pass collects any
preview-<n>that is a month old, whatever elseis true of it — label or no label, stamp or no stamp. It is found by name
(
preview-<n>, digits required, which is also what keepspreview-systemout)and its four objects are deleted by the name derived from that, because the
ones it collects may carry no label to select. A month is
ttl_secondsfrompreview-config, read and never retyped, which makes the reaper thatConfigMap's third reader; a run that cannot read it refuses and exits 1 rather
than guessing, having already done the first pass.
This is an addition, not a replacement: the console's Delete and Activate still
work through
preview-expires, a closed PR still tears down by label, andnothing is collected before it has had its month —
preview-61is a day oldand stays.
Story 15 said a preview cleans itself up, and this is what makes it true for
every preview rather than for the ones a later pipeline revision happened to
create; the story now says so. OPERATIONS.md's teardown section, the TTL
section, and drill steps 5-6 follow: an unlabelled preview with a stamp in the
past is reached by nothing and must survive a tick, and the same stamp on a
labelled one is the console's Delete and must be collected.
Nothing in the
preview-systemlayer may contain a${...}— it is appliedwith no
postBuild.substituteFrom— so the catch-all's expansions arebrace-free, and the test guarding that layer is what caught the first draft.
The reaper's existing pass is untouched: a namespace selected by `milner.dev/preview-pr` whose `milner.dev/preview-expires` has passed is collected, and every object is deleted by that label. A non-numeric or missing expiry is still skipped. That pass is byte-identical to `main` apart from a section comment. What it could never see is a preview with no label. CI runs the *branch's* pipeline, so a pull request that never rebased onto the split created its namespace with the step that predates the label, and no step revisits a namespace that already exists to add it. L3's create-only rule (the `preview-deployer` SA holds create and delete, not patch) meant there was no backfill path either. Nothing then moved such a namespace towards collection: `preview-61` sat past its four-hour stamp until someone looked, and eleven branches still carry the pipeline that mints more like it. So a second pass collects any `preview-<n>` that is a month old, whatever else is true of it — label or no label, stamp or no stamp. It is found by name (`preview-<n>`, digits required, which is also what keeps `preview-system` out) and its four objects are deleted by the name derived from that, because the ones it collects may carry no label to select. A month is `ttl_seconds` from `preview-config`, read and never retyped, which makes the reaper that ConfigMap's third reader; a run that cannot read it refuses and exits 1 rather than guessing, having already done the first pass. This is an addition, not a replacement: the console's Delete and Activate still work through `preview-expires`, a closed PR still tears down by label, and nothing is collected before it has had its month — `preview-61` is a day old and stays. Story 15 said a preview cleans itself up, and this is what makes it true for every preview rather than for the ones a later pipeline revision happened to create; the story now says so. OPERATIONS.md's teardown section, the TTL section, and drill steps 5-6 follow: an unlabelled preview with a stamp in the past is reached by nothing and must survive a tick, and the same stamp on a labelled one is the console's Delete and must be collected. Nothing in the `preview-system` layer may contain a `${...}` — it is applied with no `postBuild.substituteFrom` — so the catch-all's expansions are brace-free, and the test guarding that layer is what caught the first draft.L2 diffs the rendered preview overlay server-side. A dry run persists nothing, so the Role `preview-router` that the same render creates does not exist when the binding that references it is submitted — and `preview-deployer` holds no `bind` verb, so the server resolves the roleRef to run its escalation check and reports that resolution failure as a missing *binding*: Error from server (NotFound): rolebindings.rbac.authorization.k8s.io "preview-router" not found Measured as that identity (a roleRef naming a Role that exists creates fine), not inferred. It went unnoticed because the grant that authorizes the RBAC kinds only takes effect when Flux next reconciles preview-system, and that took a run to happen: pipeline 74 diffed at 20:34 with the old Role still live and dropped the three kinds, Flux configured the grant at 20:39, and every run since submits the binding and fails. L2 now names this class separately from the kinds the identity may not submit — a gap that closes versus a limit of the check — and leaves every other object, and every other RoleBinding, in the diff. L1 still schema- and policy-checks it; L3 applies it for real, Role first, which is why the preview itself was never broken.L3 writes `preview/oz:pr-<n>-<sha>` into the preview's values ConfigMap unconditionally — `apps/preview/oz.yaml` is in the overlay for every change set, as the preview's default front door and the page the disambiguation happens on (story 7) — but `build-oz` was path-filtered on `oz/**` and `previewlib/**` on its pull-request half too. A change set that touched neither was handed a tag nothing had built: oz 0/1 ImagePullBackOff zot.faun-snapper.ts.net/preview/oz:pr-64-5e14c89 The registry held only `pr-62-*` tags, the other three workloads in preview-64 were Running, and L3 died ten minutes later on its health-check timeout saying only "timed out waiting for the condition". The router has this gap handled — L3 asks the registry and falls back to the image the front door runs — and that fallback is right for the router, which a preview only has when the change set is about one. It is wrong for the console, which every preview has: the fallback is the *real* console (login, real cluster reads, real actions), and a preview namespace deliberately holds no credential and no cluster grant for it. So the console's pull-request half loses its path filter: 42 seconds of buildah on the change sets that do not touch `oz/**`, against a ten-minute timeout that reads like a pod problem. The main-push half keeps its filter and its pinned tag, and the docs and tests that described the old symmetry now describe the asymmetry and why it is the right shape.Pipeline 84 died in L1 after 8s, on a commit that only moved a step's position: WARNING: fetching https://dl-cdn.alpinelinux.org/alpine/v3.21/main: temporary error (try again later) ERROR: unable to select packages: curl (no such package) `apk` treats one unreachable mirror as final, so a step whose real work needs no network at all after that line is red for the CDN's mood — the mirror's own words are "temporary" and "try again later", which is exactly what a pipeline should not make a person do. Measured either side of it: pipeline 83 installed the same `curl` in the same step in 12s and 43 packages in its `test` step; pipeline 84, one commit later, failed here in 8s. The GitHub downloads in L1 already retry for the same class of flake (`--retry 5 --retry-all-errors`, with the earlier "Could not resolve host" red guard recorded above them). This is the package install's half of it, and it goes in `ci/` — glue more than one step needs — because all four `apk add` sites in the pipeline have the same exposure: L1, the suite, and both tag guards. Three attempts, five and ten seconds apart: enough for a dropped fetch or a mirror being rotated, short enough that a real outage still fails the step inside the minute. A stub apk drops two fetches and succeeds on the third (exit 0); a stub that never answers gives up by name on the third (exit 1). Against the real apk in the CI image it installs `git` and exits 0. `tests/test_static.py::test_the_pipeline_installs_packages_through_the_retry` holds that no step writes its own `apk add` — one that did would inherit the flake instead of the retry — and the shellcheck list gains the helper plus `ci/oz-image.sh` and `ci/oz-tag-guard.sh`, which it had been missing.