Skip to content

fix(apps): let the cleanup hooks watch the objects they delete - #4135

Merged
Aleksei Sviridkin (lexfrei) merged 2 commits into
cozystack:mainfrom
yankawai:fix/cleanup-hook-pvc-watch
Sep 18, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 2 commits into
cozystack:mainfrom
yankawai:fix/cleanup-hook-pvc-watch

Conversation

@yankawai

@yankawai europrinter (yankawai) commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Fixes #4085.

kubectl delete waits for the object to disappear unless --wait=false is given, and that wait is a watch on the object. The post-delete hook Roles of harbor, clickhouse, mariadb and qdrant grant get, list and delete only, so every uninstall logs

Failed to watch *unstructured.Unstructured: persistentvolumeclaims "..." is forbidden: User "system:serviceaccount:<ns>:<release>-cleanup" cannot watch resource "persistentvolumeclaims"

the wait is skipped, and the hook prints PVC cleanup complete while the claims are still terminating.

Every collection-scoped rule behind a delete in these four hooks now also carries watch: persistentvolumeclaims in all four; certificates, challenges and the solver ingresses in harbor. The four named Secrets the mariadb hook removes keep get and delete only: they carry no finalizer, so the delete never reaches the watch, and a resourceNames rule does not authorize a collection watch anyway.

A real wait needs a bound. kubectl's default is a week, and a claim held by a pod that will not stop, or a stuck detach, would hold the hook until helm-controller's 300s uninstall budget ran out and leave the HelmRelease stuck uninstalling. The PVC deletes now run with --wait=true --timeout=120s, so the expiry is reported by the script with the claims still terminating named, and each Job carries activeDeadlineSeconds: 270 with terminationGracePeriodSeconds: 10, the numbers packages/apps/tenant/templates/cleanup-job.yaml documents against that budget, as the backstop for a kubectl that never returns. --wait=false was not an option: it is exactly the report-complete-while-terminating behaviour this PR removes.

The chart unit tests are updated and pass: harbor 21/21, clickhouse 24/24, mariadb 111/111, qdrant 104/104. The behavioural suite hack/app-cleanup-hooks_test.bats passes 11/11 against the changed scripts.

The second observation in #4085, the harbor hook pod occasionally starting after its ServiceAccount is gone, is a hook-weight question and is not touched here.

Downstream repositories

Hook templates and their tests only: no values, schema, README or defaults change.

Release note

fix(apps): let the harbor, clickhouse, mariadb and qdrant cleanup hooks wait for the deleted objects, with the wait bounded so a stuck claim cannot hang the uninstall

Summary by CodeRabbit

  • Enhancements

    • Cleanup operations for ClickHouse, Harbor, MariaDB, and Qdrant now wait for persistent volume claim deletion to complete, with a 120-second timeout.
    • Cleanup jobs are limited to 270 seconds and allow 10 seconds for graceful termination.
    • Cleanup permissions now support monitoring relevant resources, including persistent volume claims and, where applicable, certificates, challenges, and ingresses.
    • Named Secret access is limited to retrieval and deletion.
  • Tests

    • Updated chart validation to verify cleanup timing, deletion behavior, permissions, and restrictions on unrelated resources.

@coderabbitai

coderabbitai Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 5edca774-d3dd-4810-a7e3-445cce83b690

📥 Commits

Reviewing files that changed from the base of the PR and between 711fcc1 and f886f26.

📒 Files selected for processing (8)
  • packages/apps/clickhouse/templates/hooks/cleanup-pvc.yaml
  • packages/apps/clickhouse/tests/cleanup_pvc_test.yaml
  • packages/apps/harbor/templates/hooks/cleanup.yaml
  • packages/apps/harbor/tests/cleanup_hook_test.yaml
  • packages/apps/mariadb/templates/hooks/cleanup-pvc.yaml
  • packages/apps/mariadb/tests/cleanup_hook_test.yaml
  • packages/apps/qdrant/templates/hooks/cleanup-pvc.yaml
  • packages/apps/qdrant/tests/cleanup_pvc_test.yaml

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Cleanup hooks

Layer / File(s) Summary
Bound cleanup execution
packages/apps/{clickhouse,harbor,mariadb,qdrant}/templates/hooks/*, packages/apps/{clickhouse,harbor,mariadb,qdrant}/tests/*
Cleanup Jobs set runtime and termination bounds. PVC deletion waits up to 120 seconds. Tests verify the deadlines, command options, and absence of --request-timeout.
Add watch permissions and validation
packages/apps/{clickhouse,harbor,mariadb,qdrant}/templates/hooks/*, packages/apps/{clickhouse,harbor,mariadb,qdrant}/tests/*
Cleanup Roles add watch access for PVCs and related Harbor resources. MariaDB removes watch from named Secret access. Tests verify the updated and restricted verb sets.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Suggested reviewers: scooby87

Merge Risk: ⚪ Minimal · up to f886f

The cleanup hooks now have bounded deletion waits and the required watch permissions, with no actionable merge risk identified.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements #4085 for Harbor and ClickHouse. It adds watch to the persistentvolumeclaims Role rules. It adds tests for the required verbs. It also makes PVC deletion use `--wait=true --timeo…
Out of Scope Changes check ✅ Passed The changes remain connected to post-delete cleanup behavior. MariaDB and Qdrant use the same PVC cleanup pattern, so their matching watch permissions, bounded waits, and tests support the same RBAC a…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main RBAC change: cleanup hooks can watch the resources they delete. It is concise and directly related to the pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug size/M This PR changes 30-99 lines, ignoring generated files labels Sep 7, 2026
kubectl delete waits for the object to disappear unless told otherwise,
and that wait is a watch. The post-delete hook Roles of harbor,
clickhouse, mariadb and qdrant granted get, list and delete only, so
every uninstall logged a forbidden watch, the wait was skipped and the
hook reported success before the PVCs were actually gone.

The rules now carry watch for every resource the hooks delete, including
the named Secrets the mariadb hook removes.

Signed-off-by: Yan Bondarenko <[email protected]>

@scooby87 scooby87 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM — one MAJOR blocker (independent cozy-review pass).

The diagnosis and the RBAC shape are right: kubectl delete defaults to --wait=true and the wait needs watch, so the missing verb made the hook skip the wait and report "complete" while PVCs were still terminating. watch is added to all Roles and the tests are green (clickhouse 3/3, mariadb 4/4).

[MAJOR] Enabling the real wait makes the post-delete hooks an unbounded teardown blocker with no limit added. clickhouse cleanup-pvc.yaml:54, harbor cleanup.yaml:106, mariadb cleanup-pvc.yaml:61, qdrant cleanup-pvc.yaml:107 all run kubectl delete pvc with the default --wait=true and no --timeout, and none of the Jobs set activeDeadlineSeconds. Before this diff the missing verb made kubectl skip the wait, so the hang was unreachable; the diff makes the wait real and the hazard attributable to this change. A stuck pvc-protection finalizer (node NotReady, stuck detach — a real LINSTOR/DRBD situation) blocks the delete forever, the Job hangs to the hook timeout, and the HelmRelease gets stuck uninstalling. Fix: a bounded wait — --timeout=<N> on the delete and/or activeDeadlineSeconds (60-300s) on the Job. Note --wait=false alone would reintroduce the original "success while terminating" bug.

[MINOR] The watch added to the mariadb resourceNames-scoped secrets rule (cleanup-pvc.yaml:135) is a dead grant: those secrets have no finalizer (deleted synchronously, watch phase never reached) and a resourceNames rule does not authorize a collection watch anyway. Remove it.

With watch granted, kubectl delete waits for the claims to be gone, and
its default for that wait is a week. A claim held by a pod that will not
stop, or a stuck detach, would hold the post-delete hook until
helm-controller's 300s uninstall budget ran out and leave the
HelmRelease stuck uninstalling.

The PVC deletes in the clickhouse, harbor, mariadb and qdrant hooks now
wait 120s at most and report the claims still terminating by name, and
each Job carries activeDeadlineSeconds 270 with a 10s termination grace
period, the numbers packages/apps/tenant/templates/cleanup-job.yaml
documents against that budget, so a kubectl that never returns cannot
hold the hook either.

The mariadb Role no longer grants watch on the named Secrets: they carry
no finalizer, so the delete never reaches the watch, and a resourceNames
rule does not authorize a collection watch anyway.

Signed-off-by: Yan Bondarenko <[email protected]>
Assisted-by: LLM
@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files and removed size/M This PR changes 30-99 lines, ignoring generated files labels Sep 17, 2026
@yankawai

Copy link
Copy Markdown
Contributor Author

Bounded in f886f26: the PVC deletes now run with --wait=true --timeout=120s, and every Job carries activeDeadlineSeconds: 270 with terminationGracePeriodSeconds: 10, the pair packages/apps/tenant/templates/cleanup-job.yaml documents against helm-controller's 300s budget. The mariadb secrets rule is back to get/delete. Chart tests 24/21/111/104, hack/app-cleanup-hooks_test.bats 11/11.

@scooby87 scooby87 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

LGTM with non-blocking notes

This is a re-review. My own prior pass (2026-09-17, on 711fcc168) was NOT LGTM with one MAJOR blocker: enabling the real wait (granting watch) turned the PVC deletes into an unbounded teardown blocker with no limit added, so a stuck pvc-protection finalizer would hang the hook to the timeout and leave the HelmRelease stuck uninstalling. The new commit f886f26ad closes it exactly as requested — --wait=true --timeout=120s on every PVC delete plus activeDeadlineSeconds: 270 and terminationGracePeriodSeconds: 10 on every Job, the same bounded-wait shape packages/apps/tenant/templates/cleanup-job.yaml already documents against the helm-controller 300s uninstall budget. My prior MINOR (a dead watch grant on the mariadb resourceNames-scoped Secrets rule) is also gone: that rule is back to ["get","delete"]. The reversal lands on changed code, not on the revision I blocked, so the flip is earned.

Verified hermetically on f886f26ad: helm unittest green (clickhouse 24/24, harbor 21/21, mariadb 111/111, qdrant 104/104) and the behavioural suite hack/app-cleanup-hooks_test.bats 11/11. The new assertions are not inert — mutating clickhouse (dropping --wait=true --timeout=120s and changing 270→260) turns the suite red on both the activeDeadlineSeconds equal and the command matchRegex, and restoring goes green.

The diagnosis and the RBAC shape are correct: kubectl delete defaults to --wait=true, that wait is a watch, and the missing verb made the hook skip the wait and report "complete" while claims were still terminating. watch is added only to the collection-scoped rules that sit behind a delete (PVCs in all four; certificates/challenges/solver ingresses in harbor), and the mariadb named-Secret rule correctly keeps get/delete only — least privilege is preserved and everything stays namespace-scoped, so there is no tenant-isolation concern.

Findings

[MINOR] clickhouse & mariadb cleanup Jobs keep the default backoffLimit (6) — a stuck PVC now surfaces as DeadlineExceeded, not the hook's own error. packages/apps/clickhouse/templates/hooks/cleanup-pvc.yaml:15, packages/apps/mariadb/templates/hooks/cleanup-pvc.yaml:12. harbor and qdrant deliberately set backoffLimit: 1 with a comment explaining that six retries would outlast the hook timeout and make "Helm report that timeout instead of the hook's own error." This PR is what makes that failure mode reachable for the other two charts: with the new 120s wait, a genuinely stuck claim burns ~120s per attempt, and under the default backoffLimit: 6 the Job hits activeDeadlineSeconds: 270 mid-second-attempt, so Helm sees DeadlineExceeded while the hook's named-PVC diagnostic (the whole point of this change) is buried in an earlier pod's log rather than being the proximate error. The 270s backstop still prevents the hang, so this is not a blocker — but adding backoffLimit: 1 to these two would make all four charts fail the same way and preserve the signal the PR is built to surface.

[NIT] harbor's Certificate / Challenge / solver-Ingress deletes now perform a real wait but carry no --timeout. packages/apps/harbor/templates/hooks/cleanup.yaml:126,153,163. With watch granted on these kinds, kubectl delete … --ignore-not-found (no --timeout) now waits with kubectl's default, which the tenant precedent documents as "a week." Only the PVC delete is explicitly bounded; these three rely solely on the Job's activeDeadlineSeconds. cert-manager objects rarely block on their finalizers and the 270s deadline caps the pathological case, so this is defensive completeness only — but bounding them (or a one-line note that the deadline is the intended bound for them) would match the PR's own "bound the wait" thesis.

Found outside this diff (non-blocking)

  • clickhouse and qdrant pin the cleanup image as docker.io/clastix/kubectl:v1.32 (mutable tag, not routed through cozy-lib.image), whereas harbor and mariadb use the digest-pinned, registry-routed cozy-lib.image form. Mirrored / air-gapped installs would fail to pull the clickhouse/qdrant cleanup image at uninstall (then hit the 270s deadline). Pre-existing, untouched by this PR.

Static-checker false positives (rejected)

  • 4× cleanup-hook-unbounded [MAJOR] (clickhouse/harbor/mariadb/qdrant): the heuristic wants --wait=false on every kubectl delete. The PR deliberately uses a bounded --wait=true --timeout=120s backed by activeDeadlineSeconds: 270 — which is both the only way to fix the actual bug (reporting complete-while-terminating) and the exact pattern packages/apps/tenant/templates/cleanup-job.yaml already establishes. The teardown-hang the invariant warns about is prevented because 270 + 10s grace < 300s helm budget. Rejected.
  • 1× hook-succeeded-on-job [MINOR] (harbor): the message claims hook-succeeded GCs a failed Job immediately. It does not — hook-succeeded deletes only on success; failed Jobs are retained (only hook-failed would GC on failure, and harbor does not set it). The harbor script is explicitly designed around keeping failed Jobs for their logs. Also pre-existing (not in the diff). Rejected.

Phase 5b (upgrade + fresh install)

These are post-delete hook templates — they render only at uninstall, so a fresh install and an in-place upgrade of an existing release are both unaffected until the operator uninstalls, at which point the new hook RBAC + bounded Job render fresh. No default flip, no RBAC contraction on running workloads, no cold-start dependency. Nothing to migrate.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit bf26ce9 into cozystack:main Sep 18, 2026
17 checks passed
@lexfrei Aleksei Sviridkin (lexfrei) added the kind/backport Categorizes issue or PR as requiring a backport to the current release line label Sep 22, 2026
@github-actions

Copy link
Copy Markdown

Created backport PR for release-1.6:

Please cherry-pick the changes locally and resolve any conflicts.

git fetch origin backport-4135-to-release-1.6
git worktree add --checkout .worktree/backport-4135-to-release-1.6 backport-4135-to-release-1.6
cd .worktree/backport-4135-to-release-1.6
git reset --hard HEAD^
git cherry-pick -x 711fcc168c5976466b66ab9d3ac029891e5327e6 f886f26ad43d305849aa78e183d008f97b4bb82b
git push --force-with-lease

myasnikovdaniil added a commit that referenced this pull request Sep 23, 2026
…ects they delete (#4369)

# Description
Backport of #4135 to `release-1.6`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/backport Categorizes issue or PR as requiring a backport to the current release line kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

harbor/clickhouse: cleanup hook Role lacks watch on persistentvolumeclaims

3 participants