Skip to content

fix(opensearch): remove the data PVCs when the application is deleted - #4136

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

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

Conversation

@yankawai

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

Copy link
Copy Markdown
Contributor

What this PR does

Fixes #4086.

The OpenSearch operator provisions its data volumes through a StatefulSet, so the PVCs carry no ownerReference, and the opensearch app chart has no cleanup hook. Removing the application therefore leaves every data volume Bound, counting against the tenant's storage quota with nothing left in the namespace that could reclaim it. clickhouse, kafka and harbor remove their operator-owned PVCs in a post-delete hook.

This adds a post-delete Job to opensearch that requests the deletion of the claims labelled opster.io/opensearch-cluster=<release> with --ignore-not-found --wait=false and exits non-zero when the request fails, with its own ServiceAccount, a Role limited to persistentvolumeclaims (get, list, delete), the clastix/kubectl image pinned by digest through cozy-lib.image, a non-root restricted securityContext, activeDeadlineSeconds: 120 so a kubectl that never returns cannot hold the uninstall, and before-hook-creation alone on the Job so a failed run and its log survive; the ServiceAccount, Role and RoleBinding keep hook-succeeded.

The delete does not wait for the claims to disappear, on purpose: OpenSearch data pods stop slowly, and a claim they still hold would keep a waiting hook running into helm-controller's 300s uninstall budget. The claims go once their pods have stopped, which is what the log says. This is the shape the monitoring, etcd and seaweedfs hooks use. Retention is not offered as a knob, consistent with the other charts; an opt-out can be added if the maintainers prefer one.

helm unittest: 64/64 in the chart, including tests/cleanup_pvc_test.yaml (hook annotations and deadline, selector and exit path, digest pin and registry prefix, securityContext, Role rules, RoleBinding). helm template renders the Job, ServiceAccount, Role and RoleBinding next to the existing objects.

Downstream repositories

A new hook template and its tests only: no values, schema, README or defaults change.

Release note

fix(opensearch): reclaim the data PVCs of a removed application with a post-delete hook

Summary by CodeRabbit

  • New Features

    • Automatically cleans up orphaned OpenSearch persistent volume claims after a release is deleted.
    • Cleanup waits for associated pods to stop and uses restricted permissions and hardened security settings.
    • Supports optional container registries with pinned cleanup images.
  • Tests

    • Added coverage for cleanup behavior, hook configuration, security settings, registry overrides, and required access permissions.

@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: 42a29b91-39ec-4577-877d-1bd81610bf2a

📥 Commits

Reviewing files that changed from the base of the PR and between 3ba3913 and 62d4a59.

📒 Files selected for processing (2)
  • packages/apps/opensearch/templates/hooks/cleanup-pvc.yaml
  • packages/apps/opensearch/tests/cleanup_pvc_test.yaml

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


📝 Walkthrough

Walkthrough

The OpenSearch chart adds a post-delete hook that deletes release-labeled PVCs. It creates a cleanup Job and supporting RBAC resources. Helm tests validate the hook configuration, security settings, image configuration, permissions, and bindings.

Changes

OpenSearch PVC cleanup

Layer / File(s) Summary
Post-delete cleanup resources
packages/apps/opensearch/templates/hooks/cleanup-pvc.yaml
Adds a post-delete Job that deletes matching PVCs. Adds the ServiceAccount, Role, and RoleBinding required by the Job. The container uses a pinned image, non-root execution, restricted capabilities, a read-only root filesystem, and resource limits.
Cleanup hook rendering tests
packages/apps/opensearch/tests/cleanup_pvc_test.yaml
Tests hook metadata, command failure handling, image digest and registry prefixing, container security settings, PVC permissions, and RoleBinding references.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant HelmRelease
  participant CleanupJob
  participant KubernetesAPI
  participant PVCs
  HelmRelease->>CleanupJob: Run post-delete hook
  CleanupJob->>KubernetesAPI: Delete PVCs matching the release label
  KubernetesAPI->>PVCs: Remove matching persistentvolumeclaims
  PVCs-->>KubernetesAPI: Return deletion result
  KubernetesAPI-->>CleanupJob: Return command status
Loading

Merge Risk: ⚪ Minimal · up to 62d4a

The reported cleanup failure does not occur under Helm’s hook lifecycle, and no actionable merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #4086 requires release PVC cleanup after OpenSearch application deletion. The new post-delete Job selects opster.io/opensearch-cluster={{ .Release.Name }} in the release namespace and runs `…
Out of Scope Changes check ✅ Passed The changes are limited to the OpenSearch PVC cleanup hook and its Helm unit tests. The hook resources directly implement issue #4086. The security settings, bounded execution, RBAC restrictions, and …
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 and concisely describes the main change: removing OpenSearch data PVCs when the application is deleted.
✨ 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 size/L This PR changes 100-499 lines, ignoring generated files 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 labels Sep 7, 2026

@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 selector is adapted correctly (not a blind clickhouse copy): opster.io/opensearch-cluster={{ .Release.Name }} is what the opensearch operator copies onto the STS-created PVCs (verified against opensearch-k8s-operator v2.8.0), so the hook is not a no-op. Security context and RBAC are tight, helm unittest 6/6.

[MAJOR] Teardown-hang vector: the post-delete Job has no spec.activeDeadlineSeconds and kubectl delete pvc has no --wait=false. templates/hooks/cleanup-pvc.yaml. kubectl delete waits for the pvc-protection finalizer by default; with drainDataNodes: true the pods shut down slowly, and if anything stalls the delete hangs forever, the Job never finishes, and the HelmRelease is stuck uninstalling. Fix: activeDeadlineSeconds (60-300s) + --wait=false. Context for calibration: the clickhouse cleanup hook this PR is modelled on shares the same gap, so ideally fix both or treat it as a consciously accepted repo pattern.

[MINOR] hook-delete-policy: before-hook-creation,hook-succeeded on the Job destroys the Job and its logs on success and orphans it on failure. The cited clickhouse example deliberately keeps only before-hook-creation on the Job (with a "keep a failed cleanup Job and its logs" comment). Drop hook-succeeded from the Job.

[NIT] watch on the PVC Role is unnecessary once the delete uses --wait=false; narrow to get/list/delete.

@yankawai

Copy link
Copy Markdown
Contributor Author

Done in 62d4a59: --wait=false on the delete, activeDeadlineSeconds: 120 on the Job, hook-succeeded dropped from the Job only, the Role narrowed to get/list/delete, and the success line no longer claims completion. 64/64.

@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

Independent cozy-review pass on head 62d4a59a. This head resolves the sole MAJOR blocker from the prior review round (self-reversal explained below — the blocking code was changed, not re-graded).

What the PR does

Adds a post-delete Helm hook (Job + ServiceAccount + namespace-scoped Role/RoleBinding) to packages/apps/opensearch that deletes the OpenSearch data PVCs when the app is uninstalled, closing the orphaned-PVC / leaked-storage bug. Brings opensearch in line with the established apps/{clickhouse,mariadb,qdrant}/templates/hooks/cleanup-pvc.yaml convention, and is in fact a hardened version of it.

Verification performed (hermetic)

  • helm unittest — 6/6 on the new suite, 64/64 for the whole chart.
  • helm template renders the hook cleanly; image resolves to clastix/kubectl:v1.32@sha256:… and picks up the _cluster.images-registry prefix.
  • Selector correctness cross-checked against the chart itself (templates/opensearch.yaml names the OpenSearchCluster {{ .Release.Name }} and uses opster.io/opensearch-cluster: {{ .Release.Name }} as its pod-level selector) and against the operator (opensearch-k8s-operator v2.8.0 copies that label onto the STS-created PVCs). So the delete selector is not a no-op.
  • allow-to-apiserver egress path confirmed: apps/tenant/templates/networkpolicy.yaml defines a CiliumNetworkPolicy granting egress to kube-apiserver for pods labelled policy.cozystack.io/allow-to-apiserver: "true", which the cleanup pod carries.

Phase 5b — upgrade + fresh install

  • Fresh install: the four hook objects are post-delete only, so they add nothing at install/upgrade time; they materialise solely on uninstall. No effect on a running cluster until it is deleted.
  • Existing-customer upgrade: purely additive (two new files). No default flips, no RBAC contraction on the running workload, no Secret/webhook/immutability traps. The new namespace-scoped Role grants only get/list/delete on persistentvolumeclaims.
  • Delete semantics: after this change, deleting an OpenSearch app destroys its data PVCs. That is the intended fix and matches the platform-wide managed-service semantic already shipped for clickhouse/mariadb/qdrant — not a behaviour regression.
  • Teardown safety: activeDeadlineSeconds: 120 + backoffLimit: 1 + kubectl delete … --wait=false bound the hook; it cannot wedge a namespace/tenant teardown. Fail-closed (exit 1) surfaces a failed cleanup rather than silently re-orphaning PVCs.

Findings

  • [MINOR] The load-bearing policy.cozystack.io/allow-to-apiserver: "true" pod-template label is not asserted by any test. packages/apps/opensearch/tests/cleanup_pvc_test.yaml. The test covers securityContext, image pinning, RBAC and the kubectl command, but not this label. It is essential: drop it and the cleanup pod cannot reach the apiserver, so kubectl delete hangs until activeDeadlineSeconds (120 s) and the Job fails — a failure that is production-only and green-in-CI, hence hard to catch on the next edit. Adding one equal assertion on spec.template.spec.template.metadata.labels["policy.cozystack.io/allow-to-apiserver"] closes the gap. Non-blocking.

  • [NIT] set -u here vs set -eu in the sibling clickhouse hook. templates/hooks/cleanup-pvc.yaml. Functionally immaterial (the single meaningful command is already guarded by if ! … ; then exit 1), so no behaviour change — noted only for cross-app consistency.

Notes / considerations (non-blocking)

  • Correctness rests on the operator labelling the STS-created PVCs with opster.io/opensearch-cluster=<cluster>; verified against opensearch-k8s-operator v2.8.0 but worth one live-cluster confirmation on the actual operator version this platform ships, since it is the linchpin of the whole fix.
  • Inherent to any post-delete PVC cleanup of an operator-managed StatefulSet: a narrow window exists where the StatefulSet controller could re-create a just-deleted PVC if the operator has not yet torn down the STS. --wait=false plus the pvc-protection finalizer make the worst case degrade to the pre-existing orphan behaviour (no new data loss). This pattern is shared verbatim with the reviewed clickhouse/mariadb/qdrant hooks and is accepted repo convention.

Self-reversal disclosure

The prior review on this PR (same reviewer, CHANGES_REQUESTED, 2026-09-17) raised one MAJOR: the post-delete Job had no activeDeadlineSeconds and the kubectl delete had no --wait=false (teardown-hang vector). Head 62d4a59a fixes exactly that — --wait=false added, activeDeadlineSeconds: 120 added, hook-succeeded dropped from the Job only, Role narrowed to get/list/delete, success message no longer over-claims. The reversal to LGTM is on changed code, not a re-grading of identical code.

The OpenSearch operator creates its data volumes through a StatefulSet,
so the PVCs carry no ownerReference, and the chart had no cleanup hook.
Removing the application left every data volume Bound and counting
against the tenant quota, unlike clickhouse, kafka and harbor, whose
post-delete hooks reclaim the operator-owned storage.

A post-delete hook in the same shape as the clickhouse one now deletes
the PVCs labelled opster.io/opensearch-cluster=<release>, with a Role
limited to persistentvolumeclaims and the kubectl image pinned by digest.

Signed-off-by: Yan Bondarenko <[email protected]>
kubectl delete waits for the claims to be gone unless told otherwise,
and OpenSearch data pods stop slowly, so the post-delete hook could
outlive helm-controller's 300s uninstall budget on a claim that would
not release and leave the HelmRelease stuck uninstalling.

The delete now runs with --wait=false, the shape the monitoring, etcd
and seaweedfs hooks use: the deletion is requested and the claims go
once their pods have stopped, which the log now says instead of
"complete". The Job carries activeDeadlineSeconds 120 as the backstop
for a kubectl that never returns and keeps only before-hook-creation,
so a failed run and its log survive. The Role drops watch, which
nothing needs once the wait is gone.

Signed-off-by: Yan Bondarenko <[email protected]>
Assisted-by: LLM
@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 08a2e76 into cozystack:main Sep 18, 2026
16 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

Successfully created backport PR for release-1.6:

myasnikovdaniil added a commit that referenced this pull request Sep 23, 2026
… application is deleted (#4368)

# Description
Backport of #4136 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.

opensearch: data PVCs are left behind after the application is removed

3 participants