Skip to content

fix(kubernetes): clear kamaji datastore-secret finalizer to unblock namespace deletion (#3062) - #3078

Merged
scooby87 merged 1 commit into
mainfrom
fix/kubernetes-datastore-finalizer
Jun 29, 2026
Merged

scooby87 merged 1 commit into
mainfrom
fix/kubernetes-datastore-finalizer

Conversation

@scooby87

@scooby87 scooby87 commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

After deleting a Kubernetes app CR / tenant, the secret
<release>-datastore-config kept the finalizer
finalizer.kamaji.clastix.io/datastore-secret, which was never removed —
blocking deletion of the entire namespace (observed stuck in Terminating
for 115 minutes). Kamaji adds this finalizer to the datastore connection Secret
and normally strips it as part of TenantControlPlane (TCP) deletion; if the TCP
is force-deleted or Kamaji does not reconcile in time, the finalizer hangs and
the Secret can never be garbage-collected, wedging the namespace.

This extends the existing pre-delete cleanup hook
(packages/apps/kubernetes/templates/delete.yaml):

  • Step 4 now deletes the TenantControlPlane with --wait=true --timeout=120s
    so Kamaji's own finalizer-cleanup chain has a chance to run before we fall
    through.
  • Step 4b (backstop) patches metadata.finalizers: null on
    <release>-datastore-config only if it still exists. By this point the TCP is
    gone, the Secret is owner-ref'd to the deleted TCP and only awaiting GC, so
    clearing the finalizer merely lets GC reap it — it cannot strip a finalizer
    while a live TCP still needs the datastore. The Secret holds only connection
    config, not tenant data (which lives in the external etcd datastore), so no
    data is lost.
  • RBAC adds a secrets rule scoped via resourceNames to exactly
    <release>-datastore-config, verbs get/patch only.

The Job is hardened: non-root (uid 65534), read-only root fs, all caps dropped,
seccompProfile: RuntimeDefault, writable /tmp for kubectl cache. Failures at
both steps are logged (the Step 4b warning explicitly mentions the namespace may
stay Terminating) but the hook still completes so it cannot itself block teardown.

This is the most impactful of the create→delete leak sweep findings: without it,
tenant teardown hangs and the create→delete cycle requires manual intervention.

Fixes #3062.

Release note

fix(kubernetes): clear the leftover Kamaji `finalizer.kamaji.clastix.io/datastore-secret` finalizer on the datastore-config Secret during tenant teardown, so deleting a Kubernetes app / tenant no longer leaves the namespace stuck in Terminating.

Summary by CodeRabbit

  • New Features
    • Extended the pre-delete cleanup workflow to more reliably remove TenantControlPlane and handle slow deletions with a bounded wait.
  • Security
    • Hardened the cleanup job by running as non-root with restricted security settings, dropped capabilities, and a safe writable temp location.
  • Bug Fixes
    • Added a conditional backstop to remove leftover finalizers from the relevant datastore Secret so namespace termination isn’t blocked.
  • Tests
    • Added a YAML test suite to validate the wait-and-patch behavior and ensure the patch runs only after the TenantControlPlane is gone.

@github-actions github-actions Bot added area/kubernetes Issues or PRs related to the tenant Kubernetes app kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files labels Jun 25, 2026
@coderabbitai

coderabbitai Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Review 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: Pro

Run ID: 32ef1b13-92d1-44e3-8bcb-34b00078a21a

📥 Commits

Reviewing files that changed from the base of the PR and between b223d36 and 4b9d67f.

📒 Files selected for processing (2)
  • packages/apps/kubernetes/templates/delete.yaml
  • packages/apps/kubernetes/tests/delete_hook_test.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/apps/kubernetes/tests/delete_hook_test.yaml
  • packages/apps/kubernetes/templates/delete.yaml

📝 Walkthrough

Walkthrough

The pre-delete cleanup hook now hardens its pod settings, waits for TenantControlPlane deletion with a timeout, and patches the Kamaji datastore-config Secret to clear leftover finalizers. The cleanup Role and YAML tests were updated to match the new secret access and deletion flow.

Changes

Kubernetes pre-delete cleanup hook

Layer / File(s) Summary
Pod hardening
packages/apps/kubernetes/templates/delete.yaml
Adds restricted securityContext settings, sets HOME to /tmp, and mounts /tmp from an emptyDir for the cleanup container.
Delete and patch flow
packages/apps/kubernetes/templates/delete.yaml
Deletes TenantControlPlane with --wait=true and --timeout=120s, then conditionally patches <release>-datastore-config to remove finalizers when present.
Secret RBAC and test coverage
packages/apps/kubernetes/templates/delete.yaml, packages/apps/kubernetes/tests/delete_hook_test.yaml
Adds scoped get/patch access for the named datastore-config Secret and adds YAML tests covering the Role scope, wait step, patch payload, and conditional finalizer removal.

Possibly related issues

  • cozystack/cozystack#3062: The new wait-then-patch cleanup flow directly matches the datastore-config Secret finalizer namespace-deletion fix.
  • cozystack/cozystack#2473: This PR updates the same pre-delete delete.yaml cleanup path for tenant deletion and Secret cleanup.

Suggested reviewers

  • kvaps
  • lllamnyp
  • androndo
  • IvanHunters
  • sircthulhu

Estimated review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A bunny hopped through cleanup night,
and nudged the finalizers out of sight 🐇
The tenant bowed, the secret sighed,
then namespace doors swung open wide.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: clearing the datastore-secret finalizer to unblock deletion.
Linked Issues check ✅ Passed The PR implements the requested fix for #3062 by waiting on TenantControlPlane deletion and removing leftover secret finalizers as a fallback.
Out of Scope Changes check ✅ Passed The changes stay focused on the pre-delete cleanup hook, scoped RBAC, security hardening, and tests for the namespace-deletion fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/kubernetes-datastore-finalizer

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.

@dosubot dosubot Bot added the priority/critical-urgent Highest priority. Must be actively worked on as someone's top priority right now label Jun 25, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical issue where Kubernetes app/tenant deletion would hang in a 'Terminating' state due to a leftover Kamaji finalizer on the datastore-config Secret. By ensuring the TenantControlPlane is fully removed before attempting cleanup and introducing a targeted finalizer-clearing backstop, the PR ensures successful namespace garbage collection. Additionally, the cleanup Job has been significantly hardened with security best practices and verified through new unit tests.

Highlights

  • Tenant Teardown Fix: Updated the pre-delete cleanup hook to wait for TenantControlPlane deletion and added a backstop to clear the Kamaji datastore-secret finalizer, preventing namespace termination hangs.
  • Security Hardening: Hardened the cleanup Job by implementing a non-root security context, read-only root filesystem, and restricted capabilities.
  • RBAC Updates: Added scoped RBAC permissions to allow the cleanup Job to get and patch the specific datastore-config Secret.
  • Testing: Added comprehensive helm-unittest cases to verify the cleanup logic and RBAC configuration.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/apps/kubernetes/tests/delete_hook_test.yaml (1)

44-46: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Pin the 120s timeout in the wait regression test.

This asserts --wait=true but not --timeout=120s, so the hook could regress to an unbounded wait while the test still passes.

Proposed test tightening
-          pattern: 'delete tenantcontrolplanes\.kamaji\.clastix\.io test-k8s[\s\S]*--wait=true'
+          pattern: 'delete tenantcontrolplanes\.kamaji\.clastix\.io test-k8s[\s\S]*--wait=true --timeout=120s'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/kubernetes/tests/delete_hook_test.yaml` around lines 44 - 46,
The wait regression test only checks for --wait=true in the delete hook command,
so it can miss a regression where timeout is removed or unbounded. Update the
matchRegex in delete_hook_test.yaml for
spec.template.spec.containers[0].command[2] to also require --timeout=120s
alongside --wait=true, keeping the assertion anchored to the delete
tenantcontrolplanes.kamaji.clastix.io test-k8s command.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/apps/kubernetes/templates/delete.yaml`:
- Around line 73-97: The delete-and-patch flow in the TenantControlPlane cleanup
is too permissive because the `kubectl delete` in the `delete.yaml` template is
allowed to fail without stopping, so the later Secret finalizer removal can run
even if the `TenantControlPlane` still exists. Update the logic around the
`kubectl delete tenantcontrolplanes.kamaji.clastix.io` command and the
subsequent `secret/{{ .Release.Name }}-datastore-config` patch so the finalizer
is cleared only after explicitly confirming the `TenantControlPlane` is absent
(for example by checking with `kubectl get` before proceeding); keep the
existing `if kubectl -n ... get secret ...` gate, but add the owner-existence
verification before the patch step.

---

Nitpick comments:
In `@packages/apps/kubernetes/tests/delete_hook_test.yaml`:
- Around line 44-46: The wait regression test only checks for --wait=true in the
delete hook command, so it can miss a regression where timeout is removed or
unbounded. Update the matchRegex in delete_hook_test.yaml for
spec.template.spec.containers[0].command[2] to also require --timeout=120s
alongside --wait=true, keeping the assertion anchored to the delete
tenantcontrolplanes.kamaji.clastix.io test-k8s command.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 52fdb9d3-3a29-41bf-82b5-b62639b5b7e9

📥 Commits

Reviewing files that changed from the base of the PR and between 63f9926 and b223d36.

📒 Files selected for processing (2)
  • packages/apps/kubernetes/templates/delete.yaml
  • packages/apps/kubernetes/tests/delete_hook_test.yaml

Comment thread packages/apps/kubernetes/templates/delete.yaml

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request updates the pre-delete cleanup hook for the Kubernetes application package to resolve namespace deletion issues (issue #3062). It hardens the security posture of the cleanup job by running it as non-root, dropping capabilities, and using a read-only root filesystem with a temporary volume. Additionally, the deletion script now waits for the TenantControlPlane to be removed and includes a fallback step to strip the leftover datastore-secret finalizer from the datastore-config Secret. RBAC rules have been scoped down to allow patching only this specific Secret, and comprehensive unit tests have been added to verify the behavior. I have no feedback to provide as the implementation is robust and follows best practices.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@scooby87
scooby87 force-pushed the fix/kubernetes-datastore-finalizer branch from b223d36 to 708b3bb Compare June 25, 2026 15:48
@myasnikovdaniil

Copy link
Copy Markdown
Contributor

Hi scooby87 did you see that issue #3018?
We recently explored same issue but chose fixing kamaji (bumping, or backporting upstream fixes) instead of fixing our specific problem. Look into this, may be you will reconsider your solution with preuninstall hooks.

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 — the finalizer-strip backstop runs in a path its own safety comment declares unsafe.

Business context: a leftover finalizer.kamaji.clastix.io/datastore-secret on the <release>-datastore-config Secret can block namespace deletion when a TenantControlPlane is force-deleted or Kamaji doesn't reconcile in time (#3062).

Blockers

B1: Step 4b strips the Secret finalizer even when the TenantControlPlane deletion timed out

File: packages/apps/kubernetes/templates/delete.yaml (Step 4 / Step 4b)
Issue: Step 4's kubectl delete tenantcontrolplanes ... --wait=true --timeout=120s || echo WARNING suppresses a timeout failure, and Step 4b then strips the Secret's finalizers gated only on the Secret existing. The Step 4b comment justifies the strip as safe because "by this point the TenantControlPlane is gone (Step 4 waited for it)" — but on timeout the TCP is not gone, so the documented precondition is violated.
Impact: (a) stripping finalizer.kamaji.clastix.io/datastore-secret while the owning TCP still exists bypasses Kamaji's datastore-secret lifecycle cleanup, risking orphaned tenant state in the shared datastore; (b) it doesn't even achieve the goal — if the TCP still exists, namespace deletion is blocked by the TCP itself, not by the Secret, so the premature strip gains nothing.
Fix: gate Step 4b on confirming the TCP is actually absent (e.g. only strip if kubectl get tenantcontrolplanes <name> returns not-found; otherwise log a warning and leave the finalizer). Keep the backstop for its legitimate case — TCP gone but Kamaji didn't strip the finalizer in time (force-delete) — just don't run it while the TCP is alive.

Note: the helm-unittest pins that --wait=true and the finalizer patch appear in the command string, but the runtime control-flow safety — the actual concern — can't be covered by helm-unittest, so it must be correct by construction.

Non-blocking

The rest is solid: the securityContext hardening, resource requests/limits, HOME=/tmp + tmp emptyDir for readOnlyRootFilesystem, and the resourceNames-scoped RBAC (get/patch on exactly <release>-datastore-config) are all correct.

…amespace deletion (#3062)

After deleting a Kubernetes app/tenant, the <release>-datastore-config
Secret kept the finalizer finalizer.kamaji.clastix.io/datastore-secret,
blocking deletion of the whole namespace. Extend the pre-delete hook to wait
for the TenantControlPlane to be removed (so Kamaji can strip the finalizer
itself), then, as a backstop, clear the finalizer on the datastore-config
Secret if it still lingers. RBAC is scoped via resourceNames to that one
Secret (get/patch). The Job runs non-root with a read-only root fs, dropped
capabilities and a writable /tmp for the kubectl cache; failures are logged.

Signed-off-by: Алексей Артамонов <[email protected]>
@scooby87
scooby87 force-pushed the fix/kubernetes-datastore-finalizer branch from 708b3bb to 4b9d67f Compare June 26, 2026 10:03
@scooby87

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) thanks for the careful review — B1 is addressed in 4b9d67f32: Step 4b is now gated on the TenantControlPlane actually being absent before the Secret finalizer is touched.

Concretely:

  • The finalizer patch now lives in an elif branch that is only reached when kubectl get tenantcontrolplanes.kamaji.clastix.io <release> returns not-found. If the TCP is still present (e.g. Step 4's --wait timed out), we log TenantControlPlane <name> still present (deletion not complete); leaving the datastore-secret finalizer untouched and leave the finalizer in place — so it is never stripped while Kamaji still owns the TCP.
  • The legitimate backstop case you called out (TCP gone, but Kamaji didn't strip the finalizer in time / force-delete) still runs.
  • Added a helm-unittest assertion that the patch is ordered behind the get tenantcontrolplanes gate. Agreed the runtime control-flow can't be fully covered by helm-unittest, so this is just a guard against regressing the ordering.

PTAL when you get a chance.

@scooby87

Copy link
Copy Markdown
Contributor Author

myasnikovdaniil thanks for the pointer to #3018 — I looked into it, and I think we're actually dealing with two distinct Kamaji finalizer bugs on two different objects, so the kamaji bump and this hook are complementary rather than alternatives:

#3018 (kamaji bump) this PR / #3062
Object DataStore (cluster-scoped) Secret <release>-datastore-config (namespaced)
Finalizer kamaji.clastix.io/TenantControlPlane finalizer.kamaji.clastix.io/datastore-secret
Upstream fix clastix/kamaji#1122 (present from 26.4.4-edge) none that I can find

I checked clastix/kamaji#1122 ("fix(ds): allow deletion for unused datastores", closes clastix/kamaji#1115): it changes only controllers/datastore_controller.go, i.e. the deletion state machine for the DataStore object's kamaji.clastix.io/TenantControlPlane finalizer. It does not touch the datastore-secret finalizer that wedges the <release>-datastore-config Secret (and thus the namespace) in our case. I also couldn't find any upstream issue/PR specifically targeting the secret finalizer.

So my read is:

If you have signal that a later kamaji version also fixes the secret-finalizer path (or that the two are coupled), I'm very happy to drop/trim this hook in favour of the bump — could you point me at it? Otherwise I'd suggest keeping both, and I can add a TODO to revisit the hook once kamaji is bumped.

(Separately: the timeout-gating concern Aleksei Sviridkin (@lexfrei) raised is now fixed in 4b9d67f32.)

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.

LGTM — B1 from the previous review is resolved.

The Step 4b finalizer strip is now gated on the TenantControlPlane being confirmed absent: if kubectl get tenantcontrolplanes <name> still finds it (Step 4 timed out), the finalizer is left untouched with an explanatory log, and the strip runs only in the elif branch once the TCP is gone — which preserves the force-delete backstop while honoring the safety precondition the comment documents. The added test pins the get-TCP gate ahead of the patch secret call.

No remaining blockers. The earlier non-blocking notes still hold (the securityContext hardening, resource limits, HOME=/tmp + tmp emptyDir, and the resourceNames-scoped RBAC are all correct).

@myasnikovdaniil

Copy link
Copy Markdown
Contributor

scooby87 I think it is may be the same thing, this secret also managed by kamaji, and also wedged because of never cleared finalizer, did you test fix that i proposed? may be we can eliminate multiple issues at once.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/kubernetes Issues or PRs related to the tenant Kubernetes app kind/bug Categorizes issue or PR as related to a bug priority/critical-urgent Highest priority. Must be actively worked on as someone's top priority right now size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

kubernetes: datastore-config secret with kamaji finalizer blocks namespace deletion

3 participants