fix(backupstrategy-controller): repair lookup-gated backup objects - #3524
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe backup strategy controller detects missing default backup objects after bucket provisioning and forces throttled Flux HelmRelease upgrades. The chart wires the gate, adds RBAC and configuration, and validates the behavior. Bucket rendering now fails on missing credentials unless offline mode is enabled. ChangesDefault backup object reconciliation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant DefaultObjectsGate
participant KubernetesAPI
participant FluxHelmRelease
DefaultObjectsGate->>KubernetesAPI: Read credentials and backup configuration
DefaultObjectsGate->>KubernetesAPI: Check referenced default objects
DefaultObjectsGate->>FluxHelmRelease: Patch requestedAt and forceAt
FluxHelmRelease->>KubernetesAPI: Reconcile missing resources
Possibly related issues
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
The default Strategy CRs and the Velero BackupStorageLocation are Helm-templated behind a `lookup` of the BucketClaim the same chart creates, and the <bucket>-<user>-credentials Secret behind a `lookup` of the COSI Secret. When those lookups are empty at install time the objects were silently skipped, and helm-controller does not re-render a release whose chart and values did not change (drift detection is off on operator-generated HelmReleases), so the skip was permanent: clusters ran for months with only BackupClass cozy-default and no Strategy CRs at all. That also fail-closes the pre-adoption snapshot in the v1.6.0 etcd migration, which reads the projected credentials Secret. Add a DefaultObjectsGate runnable to backupstrategy-controller. Once the bucket name is resolvable from the projector's source Secret, it checks that every object cozy-default routes to exists and forces one real Helm upgrade (reconcile.fluxcd.io/forceAt + requestedAt) when any is missing. Helm remains the objects' only author; the gate only makes sure the render that produces them actually happens. It cannot be a render-time `fail` in this chart: the chart is the producer of the Bucket its own lookup reads, so a failed render would deadlock the condition. The bucket chart's user-credentials template CAN fail safely — the BucketAccess that produces the COSI Secret belongs to the parent release and that release retries forever — so make it fail loudly instead of skipping, with requireUserCredentials=false as the offline-render escape. Also correct the chart and doc comments that claimed Flux re-renders on its interval, and document the two-release manual recovery for clusters already affected. Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
5fed9ea to
51ad226
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
internal/backupcontroller/default_objects_gate.go (2)
154-168: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winReuse the manager's RESTMapper instead of building a second one.
SetupWithManagercreates a newapiutil.NewDynamicRESTMapperand an HTTP client only to satisfymeta.RESTMapper. Usemgr.GetRESTMapper()unlessDefaultObjectsGateactually needs a separate discovery provider; otherwise this starts a second RESTMapper discovery cache for the same config.🤖 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 `@internal/backupcontroller/default_objects_gate.go` around lines 154 - 168, Update DefaultObjectsGate.SetupWithManager to assign g.RESTMapper from mgr.GetRESTMapper() instead of creating a separate HTTP client and apiutil.NewDynamicRESTMapper; retain the dynamic client initialization and existing error handling, and remove the now-unused RESTMapper construction.
105-118: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueUpdate the RBAC comment to include
patchfor the dynamic client.
internal/backupcontroller/default_objects_gate.gosays this field needs onlygetRBAC, butforceHelmReleasealso callsPatchon the HelmRelease. The chart already grantsgetandpatchforhelm.toolkit.fluxcd.io/helmreleases; make the comment match that usage.📝 Suggested comment fix
// Interface is a dynamic client used for the per-object existence // checks and the HelmRelease annotation patch. Going through the // dynamic client keeps these reads off the controller-runtime cache, - // so they need only `get` RBAC and start no cluster-wide informers. + // so they need only `get` (existence checks) and `patch` (forcing the + // HelmRelease) RBAC, and start no cluster-wide informers. dynamic.Interface🤖 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 `@internal/backupcontroller/default_objects_gate.go` around lines 105 - 118, Update the RBAC description above DefaultObjectsGate’s embedded dynamic.Interface to state that the dynamic client requires both get and patch permissions, reflecting forceHelmRelease’s HelmRelease patch operation while retaining the existing cache/informer explanation.
🤖 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 `@internal/backupcontroller/default_objects_gate.go`:
- Around line 189-220: Bound each periodic invocation in
DefaultObjectsGate.checkAndLog with a timeout shorter than g.Period before
passing the derived context to Check, and cancel it after the check completes.
Preserve the manager context so shutdown still propagates, and ensure the
timeout is applied to every ticker-driven check rather than only initial
startup.
- Around line 222-244: Update DefaultObjectsGate.Check’s source Secret lookup to
treat apierrors.IsNotFound(err) as an expected no-op, returning nil, false, nil
just like the empty creds.bucket case; continue propagating other Get errors
unchanged.
---
Nitpick comments:
In `@internal/backupcontroller/default_objects_gate.go`:
- Around line 154-168: Update DefaultObjectsGate.SetupWithManager to assign
g.RESTMapper from mgr.GetRESTMapper() instead of creating a separate HTTP client
and apiutil.NewDynamicRESTMapper; retain the dynamic client initialization and
existing error handling, and remove the now-unused RESTMapper construction.
- Around line 105-118: Update the RBAC description above DefaultObjectsGate’s
embedded dynamic.Interface to state that the dynamic client requires both get
and patch permissions, reflecting forceHelmRelease’s HelmRelease patch operation
while retaining the existing cache/informer explanation.
🪄 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 Plus
Run ID: ffac70f7-518f-45b5-9e5f-624cc4bc859b
📒 Files selected for processing (13)
cmd/backupstrategy-controller/main.godocs/operations/backup-classes.mdinternal/backupcontroller/default_objects_gate.gointernal/backupcontroller/default_objects_gate_test.gopackages/system/backupstrategy-controller/templates/_helpers.tplpackages/system/backupstrategy-controller/templates/backupclass-default.yamlpackages/system/backupstrategy-controller/templates/deployment.yamlpackages/system/backupstrategy-controller/templates/rbac.yamlpackages/system/backupstrategy-controller/tests/default_objects_gate_test.yamlpackages/system/backupstrategy-controller/values.yamlpackages/system/bucket/templates/user-credentials.yamlpackages/system/bucket/tests/user_credentials_test.yamlpackages/system/bucket/values.yaml
… absent Secret Two defects found in review of the periodic recovery loop. checkAndLog passed the manager's root context straight into Check, which makes several sequential API calls. That context is only cancelled at shutdown, so an API server stalling on any one of them blocked the call indefinitely. The ticker loop is sequential, so one stuck call stopped every later check for the life of the pod: the recovery this gate exists to provide went silent, leaving a stale gauge and no further log line, with only a restart to bring it back. Each check now runs under a timeout of half the Period, capped at 30s, so it can never overlap the next tick. Check also propagated a NotFound on the projector's source Secret, although the very next branch treats an empty bucket name as an expected no-op. Those are the same bootstrap state seen a moment apart, so the absent Secret logged a check failure on every tick of the whole bootstrap window for a condition the doc comment frames as graceful. Both are pinned by tests that fail against the previous behaviour: one reverts to an error on the absent Secret, the other to a timeout equal to Period. Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
|
Both findings are correct, and both are fixed in e85501e. I verified each against the code rather than taking them at face value, and they hold. Unbounded context. NotFound on the source Secret. Right, and it contradicted the doc comment two lines below, which frames an unresolvable bucket as a graceful no-op. An absent Secret and an empty bucket name are the same bootstrap state seen a moment apart, so the absent Secret now takes the same silent path. Other Two tests added, both failing against the previous behaviour: one reverts to an error on the absent Secret, the other to a timeout equal to |
VerdictNOT LGTM The static review surfaced one MAJOR code defect on a supported configuration toggle, plus a set of minor issues and unverified live behavior. Details below; every finding cites Findings[MAJOR]
Rendered corner: Fix: make the set the gate checks equal to the set that actually renders under the current values. Either gate the Velero strategyRefs in [MINOR] [MINOR] [MINOR] Claim mismatches[PARTIAL] PR body / Operational risks
Caveats
Recommended follow-ups
|
… when velero.bslEnabled=false The cozy-default BackupClass routes VMInstance/VMDisk to the Velero Strategy CRs unconditionally, but the chart gates those CRs on velero.bslEnabled. With the BSL disabled they never render, so the gate counted them as permanently missing and forced a Helm upgrade every MinForceInterval forever, climbing force_reconciles_total and pinning the missing gauge. Skip Velero-kind strategyRefs when the BSL is disabled (empty VeleroNamespace), the same flag that gates their render. The regression test now omits the manually-created Velero objects so it fails without the skip. Correct the values.yaml comment that wrongly claimed the Strategy CRs still ship in this mode. Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
…ESTMapper The BSL existence check used a hardcoded GVR and the dynamic client, whose Get returns a plain 404 (IsNotFound) for an unserved API group. The IsNoMatchError branch meant to skip an absent Velero API was therefore dead code, and if Velero were uninstalled after bootstrap while bslEnabled=true the BSL would be counted missing and force a Helm upgrade forever. Route the lookup through the RESTMapper like the strategy loop, so an absent velero.io API is a NoMatch we skip. A regression test fails without the change. Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
…n the gate fixture The gate reads the BackupClass as the manifest of what must exist, but the Go fixture listed only CNPG, Etcd and the two Velero routes while the shipped backupclass-default.yaml also routes MariaDB and Altinity. Add both to the fixture, and to the RESTMapper and list-kinds derived from it, so the expected-object set the tests exercise equals the real route set. Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
Andrey Kolkov (androndo)
left a comment
There was a problem hiding this comment.
LGTM ✅
Reviewed fix/backupstrategy-controller-lookup-gate against main. Verified locally: go build ./internal/backupcontroller/ ./cmd/backupstrategy-controller/ passes and the new gate tests are green.
The bug is real and correctly diagnosed
The default Strategy CRs, the Velero BackupStorageLocation, and the per-user credentials Secret are all Helm-templated behind a lookup of an object the same chart is still creating. On a fresh install the lookup is empty, the templates render nothing, and because helm-controller does not re-render an unchanged, successful release, the skip is permanent rather than a bootstrap window — fail-closing migration 50 (etcd v1.6.0 adoption).
Two layer-appropriate fixes, both hold up
DefaultObjectsGaterunnable — leader-elected, ticks every minute, resolves the bucket from the projector's source Secret (no newlookup, covers external-S3), readsBackupClass cozy-defaultas the manifest of required objects, and stampsforceAt/requestedAton its own HelmRelease when anything is missing. Correctly skips Velero-kind strategies whenVeleroNamespace==""(matching the chart'sand .Values.velero.bslEnabled $bucketNamegate), treats an absent CRD as not-missing viaRESTMapping+IsNoMatchError, and handles throttling, the both-annotations requirement, and NotFound tolerance for non-Flux installs. Clean off-switch viabackupStorage.reconcileDefaultObjects. RBAC delta is minimal and accurate (patchhelmreleases +getbackupstoragelocations, all through the dynamic client).bucketchart fails loudly instead of silently skipping. No deadlock: the<bucket>-systemHelmRelease setsremediation.retries: -1and theBucketAccessthat produces the COSI Secret is rendered by the parent release, sofailretries to convergence and cannot block its own precondition. Offlinehelm templateis safe (users: {}→ empty range).
Tests & docs
13 new Go test funcs cover the happy path plus error/edge paths (bucket unresolved, source Secret absent, throttling, unmapped kinds, Velero-disabled skip, BSL-API-absent skip, patch failure surfaced, absent HR tolerated, disabled config). Helm-unittest exercises the new fail behavior. The PR also corrects the stale docs that claimed the skip self-heals on the Flux interval.
No blocking findings. All reviewed lines are introduced by this diff.
Non-blocking suggestions
- The
env var == release nameand ClusterRole-verb-list asserts intests/default_objects_gate_test.yamlverify one static file against another — the real contract (gate forces the release, objects appear) is only provable by an e2e. Consider whether those two asserts earn their keep. - An e2e for the gate (bucket race → objects materialize within a minute) would be the highest-value follow-up.
- Minor log-wording nit: in
Check, the non-Flux-install branch returnsforced=false, socheckAndLoglogs "still missing, force throttled" — slightly misleading for the absent-HR case, though harmless.
VerdictNOT LGTM. The Findings[MAJOR] The
There is no per-bucket escape (see the next finding), so the operator cannot unwedge one bucket without touching cluster-wide state. The PR body justifies why Fix options: scope the failure per-user (render the resolvable users, signal the unresolved one — Helm [MAJOR] The parent HR passes only Additionally: this is a behaviour change on the fresh-install path of every bucket (silent-skip → transient [MINOR]
[MINOR] The gate patches Caveats
Checked and correct
|
…from the gate, not a chart fail Addresses the four findings on cozystack#3524. **The `fail` in the bucket chart (MAJOR).** The <bucket>-<user>-credentials Secret was made to `fail` the render while its COSI lookup was empty, so the release would retry and converge instead of skipping the Secret permanently. Helm cannot render a partial set, so that fail aborts the WHOLE <bucket>-system release: the other users' Secrets, and the bucket UI Deployment/Service/Ingress/HTTPRoute with them. One declared user whose BucketAccess never provisions takes down every other user of that bucket, and on an already-installed release parks it in Failed, blocking every later upgrade. **No per-bucket escape (MAJOR).** Rather than add one, remove the need for it. The gate already forces the release that renders the lookup-gated Strategy CRs; the credentials Secret is the same trap one release earlier, and the gate reads that Secret to resolve the bucket name in the first place. So while it is absent, or carries no bucket name, force the bucket's <bucket>-system release instead of doing nothing. That cannot deadlock its own precondition: the BucketClaim and the BucketAccess whose COSI Secret the lookup reads are rendered unconditionally by the parent release, not by the one being forced. The two releases are throttled independently — they are forced in sequence on a bootstrap, so a shared timestamp would delay the second by a full MinForceInterval for no reason. Coordinates come from the chart under provisionBucket, and are omitted on external S3 where the Secret is admin-managed and no release renders it. No RBAC delta: patch on helmreleases is already cluster-scoped. The chart goes back to a partial render, with the comment now explaining why the skip is not self-healing and why failing is the wrong lever, and a test that renders the UI with every user unresolvable so a reintroduced `fail` fails the suite. Tenant buckets keep their pre-existing behaviour; generalising the repair to every bucket needs a controller that owns them and is tracked separately. **Stale gauge (MINOR).** cozystack_backup_default_objects_missing was only written on the happy path, so the state the gate exists to catch reported 0 and the documented alert never fired. It is now written on the unresolved path too — the credentials Secret counts as one of the objects the default backups depend on, which is what it always was. On an API error the gauge is deliberately left alone rather than flapping, so a new cozystack_backup_default_objects_check_errors_total marks it stale, and the doc pairs the two instead of overstating the gauge. **Suspended releases (MINOR).** forceHelmRelease now does a point Get and skips a release with spec.suspend: true. helm-controller ignores forceAt and requestedAt while suspended (`cozyhr suspend` sets exactly that), so the gate was re-stamping every MinForceInterval for the whole suspension and climbing the force counter — which the runbook attributes to a render that is not producing the objects, the wrong diagnosis. The skip is logged and not counted, so a climbing counter keeps its documented meaning. Reported-by: IvanHunters <xorokhotnikov@gmail.com> Signed-off-by: Mattia Eleuteri <mattia@hidora.io> Assisted-By: Claude <noreply@anthropic.com>
|
All four confirmed against the code. Pushed as f859b71. The two MAJORs: the
|
|
CI note — the two red checks are not from this diff.
Everything else is green: pre-commit, DCO, CodeQL, Analyze (go), API owner review, Plan build, size, label. |
IvanHunters
left a comment
There was a problem hiding this comment.
Verdict
LGTM with non-blocking notes
Static review found no correctness, RBAC, upgrade, or config-matrix defect; the controller is HA-correct, tests are adequate and non-vacuous, and every schema-valid chart corner renders — the only open item is the live-cluster efficacy of the force mechanism, which the author already flags and which belongs to an e2e run.
Findings
[MINOR] packages/system/backupstrategy-controller/templates/deployment.yaml:54, multi-paragraph comment essays ship into the rendered manifest
Lines 54-60 and 68-90 are # YAML comments inside the container env: list, e.g. # DefaultObjectsGate: the Strategy CRs and the Velero BSL are gated. Unlike a {{/* */}} template comment, # comments are not stripped at render: I confirmed by helm template that both blocks appear verbatim in the rendered Deployment. The prose is accurate but belongs on the docs site (it duplicates docs/operations/backup-classes.md); leave a one-line pointer and move the essay out so it cannot drift from the code and does not ride into every rendered manifest.
[MINOR] packages/system/bucket/templates/user-credentials.yaml:1, 36-line explanatory essay in a template comment
The {{/* … */}} block (lines 1-36) is a genuine narrative essay. Because it is a Go-template comment it is stripped before render (so it does not reach the applied object — lower cost than the deployment.yaml case above), but the inline-prose-rot argument still applies: it re-explains the whole gate design next to a template that no longer decides anything. Reduce to a short pointer to default_objects_gate.go / the docs page and keep the design rationale in one canonical place.
Claim mismatches
[UNVERIFIABLE] "make generate — no diff": not executed (generator unavailable in the hermetic clone). Low risk: neither touched chart has a values.schema.json or README.md in-tree, and no schema/README diff is present in the PR, so there is nothing for make generate to regenerate for these charts.
Caveats
- Core mechanism is not statically verifiable and the author declares no live run. The fix rests on helm-controller honouring
reconcile.fluxcd.io/forceAt+requestedAton an otherwise-unchanged operator-generated HelmRelease AND re-running thelookups during that forced upgrade so the gated Strategy CRs / BSL / credentials Secret materialise. This is standard documented Flux behaviour and the equivalent manual stamp was hand-run on the reporter's cluster, but forcing a live Helm upgrade through helm-controller (SSA against live objects, actual re-render timing) is invisible tohelm templateandgo test. Not executed — reasoned only. Route to a live e2e run (see follow-ups). - Leader-failover / controller restart resets the in-memory throttle.
lastForce/lastCredentialsForce(default_objects_gate.go:178-179) are process-local andStartruns an immediatecheckAndLogbefore the ticker (:241), so a freshly-elected leader or a restarted pod issues one force immediately if objects are still missing, bypassingMinForceInterval. Bounded and harmless (the stamp is idempotent, a re-render is what is wanted, and steady state is a no-op), so noted, not blocking. - Verified sound and left as a checked-and-correct ledger: leader election is enforced (
--leader-electin deployment.yaml:29,replicas: 2,NeedLeaderElection()=true), so annotation-state is written only by the leader — no HA write race; RBAC delta is minimal (patchon helmreleases already cluster-wide for RestoreJob's cross-tenant rename,geton backupstoragelocations read-only);parseSourceSecretresolves the bucket name from both the flat-key and the raw COSIBucketInfoformat, so a COSI-format Secret does not drive a permanent force loop; unmapped strategy CRDs and an absent Velero API are skipped (no endless force);velero.bslEnabled=falsegates both the BSL check and the Velero strategy skip consistently.
Recommended follow-ups
- Run a live e2e on a bootstrap cluster: confirm the gate forces
bucket-<name>-system, that the credentials Secret then materialises, that the bucket name resolves, and that the second force onbackupstrategy-controllercreates the 6 Strategy CRs + thecozy-defaultBSL — within a minute or two, per the PR's own remaining-verification note. backupStrategyController.chBackupClientImagereuse ofplatform-migrations(values.yaml:2-38) is a pre-existing, already-documentedWORKAROUND/TODO(349 MB image, cross-package digest coupling) — out of scope for this PR, tracked in-file; noting so it is not lost.
|
Successfully created backport PR for |
|
Created backport PR for
Please cherry-pick the changes locally and resolve any conflicts. git fetch origin backport-3524-to-release-1.5
git worktree add --checkout .worktree/backport-3524-to-release-1.5 backport-3524-to-release-1.5
cd .worktree/backport-3524-to-release-1.5
git reset --hard HEAD^
git cherry-pick -x 51ad22670684b7bc3b7b13bb54403e1aecedc997 e85501e9f5d3dc6effe5e8721b5db073af59bdb4 aa3accd12800fd977678d771c719e89fd064ff5c bf152ba8640da08fd77f486eaa7c4714c6ba6a9d 8b006391c2ba89280096dcee8fd7f85dd3a41c7d f859b710a45d7df4bd255991ab1d980b048d352a
git push --force-with-lease |
The default Strategy CRs and the Velero BackupStorageLocation are templated behind a `lookup` of the BucketClaim the same chart creates, because the COSI driver assigns the real S3 bucket name (`bucket-<claim-UID>`) and it cannot be computed at render time. On a fresh install that lookup is empty, so those templates render nothing. helm-controller does not re-render a release that succeeded and whose chart and values did not change, and drift detection is off on operator-generated HelmReleases, so the skip is permanent: a cluster that loses the install-time race keeps `BackupClass cozy-default` with no Strategy CRs and no Velero BSL indefinitely. Add a DefaultObjectsGate runnable that, once the bucket name is resolvable from the credentials projector's source Secret, verifies that every object the BackupClass routes to exists and stamps both reconcile.fluxcd.io/forceAt and reconcile.fluxcd.io/requestedAt on the HelmRelease to force the real Helm upgrade that re-runs the lookups. requestedAt alone is a no-op for an unchanged release, so both annotations are required. Two Prometheus metrics expose the state independently of the HelmRelease Ready condition. Also make the per-user bucket credentials template fail loudly instead of silently skipping when the source Secret is absent, so this class of race surfaces as a failed release rather than as a missing Secret. Backport of #3524 to release-1.5. The conflict in docs/operations/backup-classes.md was resolved to the post-fix wording: the pre-fix text claimed Flux repairs the skip on its interval reconcile and told operators to run `flux reconcile helmrelease`, which is precisely the misconception this change corrects. Signed-off-by: Mattia Eleuteri <mattia@hidora.io> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
What this PR does
Fixes #3518.
Objects in
packages/system/backupstrategy-controllerandpackages/system/bucketare gated on a Helmlookupof an object that is still being created. When the lookup is empty the object is skipped — and since helm-controller does not re-render a release whose chart and values did not change (drift detection is off on operator-generated HelmReleases), the skip is permanent, not a bootstrap window. On the reporter's cluster the install-timing window was 4 seconds and the objects were still missing months later:StrategyCRs and the VeleroBackupStorageLocation, gated oninclude "backupstrategy-controller.bucketName"→ alookupofBucketClaim;<bucket>-<user>-credentialsSecret (packages/system/bucket/templates/user-credentials.yaml), gated on alookupof the COSI Secret.Only
BackupClass cozy-defaultwas present, because it renders unconditionally. Its own comment predicted this case while assuming "Flux re-renders the chart", which does not happen — the same trappackages/core/platform/sources/cozystack-basics.yamlalready documents: "Drift detection is off on operator-generated HelmReleases, so a capability gate in the templates would render the policy out at first install and never add it back."Consequence: migration 50 (etcd adoption, v1.6.0) is fail-closed on the snapshot target. It resolves the target from the projected credentials Secret, which has no source without
<bucket>-<user>-credentials, so its Job reboots forever and the cluster cannot be upgraded at all.1.
backupstrategy-controllerreconciles the objects into existenceNew leader-elected
DefaultObjectsGaterunnable (internal/backupcontroller/default_objects_gate.go), modelled on the existingSystemCredentialsProjector. Every minute:parseSourceSecret) — no new dependency, nolookup, and it covers the external-S3 path too. While it is unresolvable the gate does nothing: forcing then would only re-run the same empty lookup;BackupClass cozy-defaultas the manifest of what must exist (itsstrategyRefs), plus thecozy-defaultBSL whenvelero.bslEnabled;reconcile.fluxcd.io/forceAtandrequestedAton its own HelmRelease. Both are required:requestedAtalone only requests a reconcile, which is a no-op for an unchanged release;forceAtis what triggers a real Helm upgrade, and it is only honoured together withrequestedAt;strategyRefwhose CRD is not installed is not counted as missing, so a missing CRD cannot drive an endless force loop.Two new metrics:
cozystack_backup_default_objects_missing{backupclass}— the alert that would have caught this, since it is non-zero regardless of the HelmRelease'sReadycondition — andcozystack_backup_default_objects_force_reconciles_total.RBAC delta is two lines:
patchonhelmreleases(getwas already there) andgetonbackupstoragelocations. Both accesses are point Gets through the dynamic client, so no cluster-wide informer and nolist/watch.Off switch:
backupStorage.reconcileDefaultObjects: false.Why not a render-time
failin this chart: it would deadlock.templates/bucket.yamlin this same chart is the producer of theBucketwhoseBucketClaimthe lookup reads, so a failed render never applies it and the condition can never resolve.Why not template the objects in Go: their bodies are values-driven (endpoint, region,
forcePathStyle, the Altinity strategy's whole PodTemplateSpec). Helm stays their single author; the gate only makes sure the render that produces them actually happens.Why not
dependsOn: an edge guarantees theBucketCR is applied, not that its claim is reconciled, so it does not close the race.2. The gate also repairs the credentials Secret, one release earlier
The
<bucket>-<user>-credentialsSecret is the same trap as the Strategy CRs, one release up the chain:packages/system/bucket/templates/user-credentials.yamlrenders it behind alookupof the COSI Secret, and skips it just as permanently. It is also the first domino — the projector reads it, so without it there is nocozy-backups-creds, no strategy, no Velero, and no migration 50.An earlier revision of this PR made that render
failso the release would retry and converge. @IvanHunters pointed out that Helm cannot render a partial set, so thefailaborts the whole<bucket>-systemrelease — the other users' Secrets and the bucket UI Deployment/Service/Ingress/HTTPRoute with them. One declared user whoseBucketAccessnever provisions would take down every other user of that bucket, and on an installed release park it inFailed, blocking every later upgrade. That was a real blast-radius regression on a universal path, and it is gone.The repair now lives in the gate, where it is per-object:
forceAt+requestedAton the bucket'sbucket-<name>-systemHelmRelease instead of doing nothing;BucketClaimand theBucketAccesswhose COSI Secret that lookup reads are rendered unconditionally by the parent release (packages/apps/bucket/templates/bucketclaim.yaml), not by the one being forced;patchonhelmreleasesis already cluster-scoped;MinForceIntervalfor no reason;provisionBucket, and are omitted on external S3, where the Secret is admin-managed and no release renders it.The bucket chart keeps its partial render. Its comment now explains why the skip is not self-healing and why failing is the wrong lever, and a test renders the UI with every user unresolvable so a reintroduced
failfails the suite.Scope this narrows, deliberately: tenant buckets keep the pre-existing silent skip. The gate owns only the platform bucket, which is the cluster-breaking path. Generalising the repair to every bucket needs a controller that owns tenant buckets and is a separate change.
Two more corrections from the same review:
cozystack_backup_default_objects_missingwas only written on the happy path, so the state this PR exists to catch reported0and the documented alert never fired. The credentials Secret now counts inmissing, so an absent or rotated-away Secret moves the gauge. On an API error the gauge is deliberately left alone rather than flapping, and a newcozystack_backup_default_objects_check_errors_totalmarks it stale; the doc pairs the two instead of overstating the gauge.force_reconciles_totalgainednamespace/name, since two releases can now be forced.forceHelmReleasedoes a pointGetand skips a release withspec.suspend: true. helm-controller ignores both annotations while suspended (cozyhr suspendsets exactly that), so the gate was re-stamping everyMinForceIntervalfor the whole suspension and climbing the force counter — which the runbook attributes to a render that is not producing the objects. The skip is logged and not counted.3. Corrected comments and docs
_helpers.tpl,backupclass-default.yaml,values.yamlanddocs/operations/backup-classes.mdall claimed the skip self-heals on the HelmRelease interval, and the doc suggested aflux reconcilethat does not re-render. They now say it does not, and why. The two-release manual recovery for already-affected clusters is documented, including the easy-to-miss part: the credentials Secret is rendered by thebucket-<name>-systemrelease, not bybucket-<name>.Manual recovery for already-affected clusters
Only needed on a version without the gate. A plain
flux reconcile helmreleasedoes nothing — it does not re-render.Validation
go test ./internal/... -count=1— pass, including 15 cases for the gate: force on missing, no-op in the steady state, throttling, unmapped kinds ignored, BSL skipped when disabled, patch failure surfaced without advancing the throttle, absent HelmRelease tolerated, plus force-on-absent-Secret, force-on-empty-bucket-name, external-S3 reports-but-does-not-force, independent throttles, suspend skip on each of the two releases, gauge written on the unresolved path, and errors counter on a failed check.make helm-unit-tests— pass, with 2 new suites: 3 cases for the bucket chart (partial render preserved, and the UI still renders with every user unresolvable — the guard against reintroducing thefail), 7 for the gate's env + RBAC wiring. Those include that.Release.Nameand.Release.Namespacematch the HelmRelease the operator generates (releaseNamebecomes the HR'smetadata.name, and nospec.releaseName/targetNamespaceis set, so the Helm release name equals the HR name), that the bucket release coordinates track a renamed bucket together withsystemSecretName, and that they are unwired onprovisionBucket: false.make generate— no diff.make go-unit-tests rd-presets-check test-check-readiness migrations-target-check— pass.make bats-unit-tests— every suite passes excepthack/migration-seaweedfs-db-adopt.bats, which needs a Docker daemon not available in this environment ("docker is required: these tests run the migrations inside alpine"). Unrelated to this diff.Not verified — please read this before approving. There was still no live-cluster run. What that gap covers is smaller than it was: dropping the chart
failremoved the universal behaviour change on every bucket's install path, so there is no longer a render-blind failure mode whose convergence has to be observed. What remains is one mechanism —forceAt+requestedAton an unchanged release — applied to two releases instead of one, argued from read code rather than an observed install:Strategy.Name = RetryOnFailureon operator-generated HelmReleases (internal/operator/package_reconciler.go), andbucketName = "bucket-" + claim.UIDinpackages/system/objectstorage-controller/images/objectstorage/patches/92-bucketclaim-propagate-ready.diff. It is the same procedure as the manual recovery below, which has been run by hand on the reporter's cluster. Someone with a bootstrap cluster should still confirm that the gate fires within a minute or two of the bucket becoming ready, and that forcingbucket-<name>-systemmaterialises the credentials Secret.Screenshots
Not applicable: no UI change. This PR touches a controller, two system charts, and docs.
Downstream repositories
Walked file by file against the diff of this PR.
website— affected, follow-up issue opened.content/en/docs/next/operations/services/backup-classes.mdmirrors this repo'sdocs/operations/backup-classes.mdand carries the exact stale claim this PR corrects, plus aflux reconcile helmreleaserecovery suggestion that has no effect.operations/configuration/platform-package.mdenumerates the forwardedbackupStoragekeys, andreconcileDefaultObjectsis admin-settable throughspec.components.platform.values.backupStorage(the whole block is deep-merged), so that list becomes incomplete. Filed as an issue rather than a PR because the wording depends on the shape this PR lands in, and whether to touch the frozenv1.5/v1.6copies is a maintainer call. No existing website issue or PR covered it.terraform-provider-cozystack— not affected. Checked, not assumed:packages/apps/**is untouched by this diff (git show --stat HEAD -- packages/apps/is empty), so no appvalues.schema.json, novalues.yamldefault, no version enum, and noApplicationDefinitionkind/plural/release.prefixchanged.reconcileDefaultObjectsis a key on a system chart, which has novalues.schema.jsonand is not modelled by the provider. (An earlier revision addedrequireUserCredentialsto thebucketsystem chart; that is gone, and no key was added toapps/bucket, so the app API is untouched.) The<bucket>-<user>-credentialsSecret the provider looks up by literal name is not renamed — this PR only changes whether it gets created, which if anything removes a case where the provider silently read null from a Secret that never existed.Everything else: no
hack/layout or make-target change (ccp,external-apps-example,cozyhr), noApplicationDefinitionCRD change, no namespace rename, no node prerequisites inhack/e2e-prepare-cluster.bats(talm,ansible-cozystack), nocozyhr.cozystack.io/values-filesannotation or chart-source-kind change, noservice-proxy-name/wholeIP/allowICMPrename (cozy-proxy), no telemetry metric or label rename (cozystack-telemetry-server— the two metrics here are new, not renamed), no node or network requirement change (examples).Release note
Summary by CodeRabbit
New Features
Bug Fixes
Documentation