Skip to content

fix(operations): replace the component removal recipe that undoes itself - #684

Open
Aleksei Sviridkin (lexfrei) wants to merge 3 commits into
mainfrom
fix/component-removal-recipe
Open

Aleksei Sviridkin (lexfrei) wants to merge 3 commits into
mainfrom
fix/component-removal-recipe

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

kubectl delete hr -n <namespace> <component> is documented as the way to remove an installed component. It removes nothing, and it is not harmless. The Package that produced the release owns the HelmRelease through a controller ownerReference, and the operator watches them with Owns(&helmv2.HelmRelease{}), so the delete fires the reconcile that recreates it. But buildHelmReleaseSpec sets no uninstall policy and no suspend, so Flux's default applies and the release is uninstalled on the way out. The workloads come back with the reinstall. Anything the uninstall destroyed does not, and the restored release hides that it happened.

Deleting the Package is what cascades into the uninstall deliberately. The disabledPackages entry has to land first, or the next platform upgrade renders the Package again and the removal undoes itself one level up. The page checks that by reading the list back from the cozystack-platform HelmRelease, since the operator copies the Platform Package's values into that HelmRelease's spec.values. The obvious alternatives can't fail: a condition wait returns instantly against the Ready left standing by the previous reconcile, and re-reading the Package after deleting it returns NotFound either way. Nothing holds a finalizer on the Package, so the delete returns before the uninstall runs. A kubectl wait --for=delete on the HelmRelease tells the reader when the destructive part is done.

What the uninstall takes is stated above the command, since making the recipe work is what arms it. packages/system/metallb/charts/metallb/charts/crds/templates/crds.yaml renders its CRDs as ordinary subchart templates with no resource-policy annotation, so helm uninstall deletes them and the apiserver cascades every IPAddressPool, L2Advertisement and BGPPeer with them. From v1.5 the frr-k8s subchart adds its own CRDs under another group, so the warning points at the chart and its subcharts rather than listing kinds. cert-manager-crds sets crds.keep and its CRDs survive the same uninstall. Through v1.4, cozystack-basics renders the tenant-root Namespace the same way, so the warning on those pages says its removal takes the root tenant. From v1.5 that namespace carries the platform.cozystack.io/no-delete label.

v1.0 gets different text, not a copy. That line never got the helm.sh/resource-policy: keep annotation on rendered Packages (none in packages/core/platform/templates/_helpers.tpl at v1.0.8), so adding a name to disabledPackages already removes an installed component there, in one step and with no confirmation. The page says that, carries the same warning, and drops the kubectl delete hr recipe. v0 predates Package objects and is left alone.

keep reached v1.1 and v1.2 mid-line, in v1.1.5 and v1.2.1. Those two pages state the single-step behaviour of v1.1.0 to v1.1.4 and v1.2.0 in its own paragraph ahead of the two-step recipe.

The v1.0 and v1.1 disabledPackages examples, on the components page and in the Hetzner guide, listed bare metallb / hetzner-robotlb. Those match nothing against the fully-qualified names the chart feeds the helper, so they're qualified now.

Not taken: cozypkg del resolves dependents and deletes in reverse topological order, which raw kubectl delete does not, but whether that subcommand exists varies across the version trees.

hugo --gc --minify builds clean and hack/check-i18n.sh passes.

@netlify

netlify Bot commented Sep 6, 2026

Copy link
Copy Markdown

Deploy Preview for cozystack ready!

Name Link
🔨 Latest commit 35a2827
🔍 Latest deploy log https://app.netlify.com/projects/cozystack/deploys/6ab2d718e359890009da22a5
😎 Deploy Preview https://deploy-preview-684--cozystack.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 1b519e8a-3e40-4c72-b0cd-60f9b02aed6e


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.

@IvanHunters IvanHunters 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

NOT LGTM

Reviewed at 2e750d69f against merge-base 257bbbbe3.

The fix itself is sound. I re-derived every factual claim in the PR body against the platform source at the matching tags rather than taking them on trust: Package ownership of the HelmRelease, Flux's default deletion behaviour, metallb's CRDs carrying no resource-policy, cert-manager's contrasting crds.keep, and the exact patch releases where the keep annotation appears. All of it holds. What blocks is that v1.0 still documents the recipe being replaced everywhere else, with no warning on the page.

Findings

  • [MINOR] content/en/docs/next/operations/configuration/components.md:83, Warning's named CRD list undercounts the blast radius under metallb.frrk8s.enabled: true
  • [MINOR] content/en/docs/next/operations/configuration/components.md:89, No signal for when the destructive uninstall has actually finished
  • [MAJOR] content/en/docs/v1.0/operations/configuration/components.md:70, the recipe this PR replaces is still live on v1.0, with no warning

Claim mismatches

[UNVERIFIABLE] "hugo --gc --minify builds clean". I could not reproduce it here: the build hung on Hugo Modules resolution, with the docsy module never appearing in GOMODCACHE despite proxy.golang.org being reachable, and was killed after two attempts. The sandbox Hugo is v0.153.2, below the 0.164.0 extended that CONTRIBUTING.md requires, which may be why. Secondary evidence supports the claim without settling it: the {{% alert %}} shortcode form used in the diff appears identically in 392 other places on the site, and open and close tags balance 1:1 in all seven touched files. A gap on my side, not a defect.

Caveats

  • v0/operations/configuration/components.md also still carries kubectl delete hr, and that one is correctly out of scope. v0 predates the Package mechanism, so the removal path there is different, and CONTRIBUTING.md explicitly demotes it to legacy-only fixes. v1.0 has neither property.
  • hack/check-i18n.sh fails today on the four stale translations the PR body names (content/{de,hi,ru,zh-cn}/_index.md). None are touched here, so it is pre-existing and matches the body's own disclosure.

Recommended follow-ups

  • File the tracking issue for the v1.0 gap if it does not get folded into this PR.
  • Consider replacing the hand-enumerated kind list in the Warning with a pointer to whatever CRDs the component's chart bundles, so a component like metallb with frr-k8s does not need the list maintained per component.

Findings not anchored to changed lines

These reference code outside this PR's diff (unchanged files, or lines outside a hunk), so GitHub cannot render them inline.

[MAJOR] content/en/docs/v1.0/operations/configuration/components.md:70 the recipe this PR replaces is still live on v1.0, with no warning

v1.0/operations/configuration/components.md:70 still tells the reader to run kubectl delete hr -n <namespace> <component>, and that page carries no warning at all. v1.0/install/providers/hetzner.md:347 still has the bare - metallb under disabledPackages that this PR fixes on the v1.1 equivalent, and the platform matches disabledPackages against the fully-qualified cozystack.metallb, so the bare name does nothing.

On v1.0 the rendered Package never carried helm.sh/resource-policy: keep, which is the same reason the PR gives for the v1.1/v1.2 caveat. So the v1.0 page documents the destructive path with zero disclosure:

$ grep -n 'kubectl delete hr' content/en/docs/v1.0/operations/configuration/components.md
70:kubectl delete hr -n <namespace> <component>
$ grep -c 'alert title="Warning"' content/en/docs/v1.0/operations/configuration/components.md
0
$ grep -n -A1 'disabledPackages' content/en/docs/v1.0/install/providers/hetzner.md | sed -n '3,4p'
346:              disabledPackages:
347-                - metallb

$ for t in v1.0.8 v1.1.4 v1.1.5; do printf '%-8s ' $t; \
    git show $t:packages/core/platform/templates/_helpers.tpl | grep -c resource-policy; done
v1.0.8   0
v1.1.4   0
v1.1.5   2

v1.0 is not demoted. hugo.yaml carries exactly one hidden: true and it belongs to next, not to v1.0; CONTRIBUTING.md demotes only v0 ("Treat v0/ as legacy"), and its own routing table says a fix that also applies to an older supported version gets copied into each relevant vX.Y/.

The PR body scopes this out on purpose and explains why (v1.0 needs a different correction rather than a copy, since disabledPackages alone still prunes the Package there). That reasoning is sound, and I verified it. What is missing is the follow-up: no tracking issue, no companion change, and no notice on the page. Either extend this PR to v1.0 with the version-scoped variant of the same two paragraphs, or file the issue before merge.

Then delete the Package object. `kubectl get packages` lists the names.

{{% alert title="Warning" color="warning" %}}
Deleting the Package uninstalls the component's Helm release, and that destroys more than the workloads. Anything the chart rendered as an ordinary template without `helm.sh/resource-policy: keep` goes with the release, CRDs and namespaces included, and Kubernetes deletes every custom resource of those CRD kinds along with them. Removing `cozystack.metallb` takes the MetalLB CRDs and with them every IPAddressPool, L2Advertisement, BGPPeer and the rest of those kinds cluster-wide; removing `cozystack.cozystack-basics` takes the `cozy-public` namespace and everything stored in it. Back up anything you still need first.

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.

[MINOR] Warning's named CRD list undercounts the blast radius under metallb.frrk8s.enabled: true

The alert names IPAddressPool, L2Advertisement, BGPPeer as the destroyed kinds. packages/system/metallb/charts/metallb/charts/frr-k8s/charts/crds/templates/*.yaml renders four more CRDs (frrconfigurations, frrnodestates, bgpsessionstates, frrk8sconfigurations) under a different group, frrk8s.metallb.io, as a nested subchart of the same cozystack.metallb release, and metallb.frrk8s.enabled: true is a documented toggle. The preceding generic sentence already covers this correctly, so this is not wrong, just incomplete for BGP-mode operators reading only the named list. Same text is duplicated identically at v1.1:78, v1.2:83, v1.3:83, v1.4:83, v1.5:83, v1.6:83.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dropped the kind list. The sentence now points at the chart and says subcharts count, so it does not need updating per component.

One scope correction: frr-k8s only landed in packages/system/metallb at v1.5.0, so v1.1 through v1.4 never shipped those four CRDs. The new wording holds on all of them anyway.

The namespace a component installs into is the exception: the operator applies that one itself, outside the component's release and with no ownerReference, so the uninstall never had it to remove.

```bash
kubectl delete package.cozystack.io <package-name>

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.

[MINOR] No signal for when the destructive uninstall has actually finished

internal/operator/package_reconciler.go registers no finalizer on Package (verified: the only finalizer usage in the tree belongs to an unrelated tapmaterializer reconciler), so kubectl delete package.cozystack.io returns as soon as the Package object itself is gone. The owned HelmRelease's deletion, and the helm uninstall inside it that destroys the CRDs/namespaces the Warning describes, proceeds asynchronously afterward via the ownerReference cascade. The recipe has no follow-up check for confirming the destructive step is actually done. Same text is duplicated identically at v1.1:84, v1.2:89, v1.3:89, v1.4:89, v1.5:89, v1.6:89.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a wait after the delete:

kubectl wait --for=delete helmrelease/<component> --namespace <namespace> --timeout=10m

with a line saying why the delete returns early.

@lexfrei

Copy link
Copy Markdown
Contributor Author

IvanHunters folded v1.0 into this PR instead of filing the issue.

It needed different text, not a copy. At v1.0.8 packages/core/platform/templates/_helpers.tpl has no helm.sh/resource-policy: keep, so disabledPackages alone already removes an installed component there: one step, no second command, no confirmation. The page now says that, carries the same warning, and the kubectl delete hr recipe is gone. Bare metallb / hetzner-robotlb in v1.0 hetzner.md and the components example are qualified too.

i18n-lint was red on four stale home-page translations, nothing to do with this PR. Fixed in e25136bd: the English home page gained a Live Demo block and de/hi/ru/zh-cn never got it.

Removing an installed component was documented as `kubectl delete hr
-n <namespace> <component>`. The Package that emitted the release
owns it through a controller ownerReference, and the operator
watches HelmReleases with Owns(), so the delete fires the reconcile
that recreates it. The command reports success, nothing logs an
error, and the component is running again afterwards.

That is not the harmless no-op it looks like. The operator sets no
uninstall policy and no suspend on the HelmReleases it builds, so
Flux's default applies and the release is uninstalled when the
object goes away. Anything the uninstall destroys is gone before the
reinstall brings the workloads back, and the restored release hides
that it happened. The page now says so, for the benefit of anyone
who already ran the old recipe.

Deleting the cluster-scoped Package is what cascades into the
uninstall deliberately. It needs the disabledPackages entry to land
first, or the next platform upgrade renders the Package again and
the removal undoes itself one level up. The reader confirms that by
reading the disabledPackages list back off the platform HelmRelease,
which is the only observation here that cannot go green early. A
condition wait returns instantly against the Ready left standing by
the previous reconcile; re-reading the Package after deleting it
returns NotFound unconditionally; and comparing observedGeneration
against generation on that HelmRelease is equal in exactly the
window that matters, because the operator has not yet copied the
edit across from the Package the reader changed.

What the uninstall takes is stated above the command rather than
after it, in a warning callout. Anything the chart rendered as an
ordinary template and did not annotate resource-policy: keep goes
with the release, CRDs and namespaces alike, and the apiserver
cascades every custom resource of those CRD kinds. Removing
cozystack.metallb takes the MetalLB CRDs and the objects of those
kinds cluster-wide; removing cozystack.cozystack-basics takes the
cozy-public namespace, which that chart renders as a plain template
and which is not its install namespace. The one exception is the
namespace a component installs into: the operator applies that
itself, outside the release, so the uninstall never had it.

The v1.1 and v1.2 pages qualify the step count in place, next to the
instruction it governs, because the behaviour changed inside those
lines rather than between them. The keep annotation on Packages
arrived in v1.1.5 and v1.2.1 as backports, so v1.1.0 through v1.1.4
and v1.2.0 have no annotation and adding the name to
disabledPackages removes the component by itself, putting the
destruction on that step instead of the delete. The clauses name
their range rather than saying "and earlier", which would be false
across lines: v1.1.5 through v1.1.7 are lower-versioned than v1.2.0
and do carry the annotation.

Applied to next and v1.1 through v1.6. Left out v1.0, whose line
never received the annotation at all, so that page needs the
single-step correction rather than this one. v0 predates Package
objects: its platform chart renders HelmReleases directly, so
deleting the HelmRelease there names the object the chart creates,
though that page carries no destruction warning either.

The v1.1 disabledPackages examples listed bare names, which match
nothing against the fully-qualified names the platform chart feeds
the helper. Both copies are fixed: the one in the components page,
and the one in the Hetzner install guide that page links to as its
worked example. Leaving the second would have had v1.1 teach the
qualified form on one page and hand out the broken form on the page
it cites, on the one provider where swapping MetalLB for RobotLB is
mandatory.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The named CRD kinds in the warning cover the top-level MetalLB chart
only; a subchart such as frr-k8s brings its own group, so the list
undercounts what an uninstall takes. Point at the chart instead of
enumerating kinds, which also stops the list needing maintenance per
component.

Package carries no finalizer, so deleting it returns before the Helm
uninstall that follows it has done anything. Add the wait that tells an
operator the destructive part is over.

Through v1.4 the cozystack-basics chart also renders the tenant-root
namespace as a plain template with no keep annotation, so uninstalling
it takes the whole root tenant, not just cozy-public. From v1.5 that
namespace carries the no-delete label the platform's admission policy
enforces, so the later pages keep the shorter example.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
v1.0 documented `kubectl delete hr` as the way to remove an installed
component, with no warning about what an uninstall takes with it. That
command destroys the CRDs and every custom resource of those kinds, then
the Package recreates the HelmRelease and the chart reinstalls, so the
workloads come back and the data does not.

v1.0 needs a different correction from later versions rather than a copy
of it: the platform does not annotate rendered Packages with
`helm.sh/resource-policy: keep`, so `disabledPackages` alone already
removes an installed component there, in one step and with no
confirmation.

Also qualify the package names in the Hetzner and components examples.
`disabledPackages` and `enabledPackages` are matched against the full
`cozystack.<name>` form, so the bare names did nothing.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei

Copy link
Copy Markdown
Contributor Author

IvanHunters I rebased onto main, so the SHAs in my earlier replies are gone. The i18n fix I referenced as e25136bd landed on main separately and is no longer part of this PR. The frr-k8s kind list and the missing wait are now in bba3f9cb. The v1.0 correction is in 35a28272.

Two changes since you looked. On v1.1 and v1.2 the note about v1.1.0 to v1.1.4 and v1.2.0 sat in the middle of the two-step instructions and read as if one of the steps applied to the old patches, so it's now a separate paragraph ahead of the steps. On v1.0 to v1.4 the warning now says that removing cozystack.cozystack-basics takes tenant-root along with cozy-public. Until v1.5.0 that chart renders the tenant-root Namespace with no keep, and v1.5.0 adds the platform.cozystack.io/no-delete label that the admission policy enforces.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants