Repository navigation
fix: re-initialize ack for wiped repositories (#435), populator fan-out with per-PVC source paths (#443), helm README (#436) - #449
Merged
Conversation
…443) A populator Restore is claimed by EVERY PVC whose spec.dataSourceRef names it, and each claimant needs its own prime PVC, mover Job and kopia source path. Add the API surface that makes that representable: - RestoreStatus.claims: BTreeMap<pvc, RestoreClaimStatus> — a MAP, because an RFC-7386 merge patch merges map keys but replaces arrays, so N concurrent populate movers can never clobber one another (the verificationStamps precedent). Absent for direct targets, so a pvc/pvcRef restore's status wire is byte-identical. - RestoreClaimPhase (Pending|Populating|Rebinding|Populated|AlreadyBound| Failed|Unknown) via the phase_serde!/PhaseLabel machinery, registered in xtask::phases::API_PHASE_ENUMS so check-phases polices every branch. - FromPolicy.sourcePath: the escape hatch for an ambiguous derivation, shape-checked at admission with the same validate_source_path the identity kernel re-applies. And the cross-volume fix itself, expand::restore_source_path: re-derive a member's kopia path from the policy's own sourcePathStrategy through the SAME EffectiveSource::kopia_source_path the backup used, so the strings cannot drift. Fails closed (naming fromPolicy.sourcePath) when selector sources disagree or share one sourcePathOverride — restoring with no path matches the newest snapshot of ANY member and could fill a volume with another volume's data. expand::populate_job_name gives each claimant a distinct, 63-capped Job name keyed on the claiming PVC's uid.
) A populator Restore now runs N concurrent movers, one per claiming PVC. Writing the top-level status.phase/resolved/logTail/failure from each of them would clobber the siblings, and the controller's pinned-resolution read would hand claim B the snapshot claim A resolved — the #443 hazard one layer up from the source path. TargetRef gains claimKey (serde default, so an in-flight Job's older work spec still decodes; omitted when unset, so every other run's wire is byte-identical). When it is set, KubeStatusReporter nests patch, pin_resolved and read_resolved under status.claims.<key>: a JSON merge patch merges map keys, so no mover can touch a sibling's entry. The claim-scoped body deliberately DROPS phase — the controller derives each claim's phase from the observed handshake and aggregates them into the Restore's own, so a mover-asserted claim phase could only fight it (the same reasoning that keeps phase off the progress heartbeat). And read_resolved never falls back to the top-level pin for a claim: that value belongs to no claim in particular. The two bootstrap TargetRef literals in repository.rs/cluster_repository.rs take a one-line claim_key: None; Rust struct literals leave no way to add a field without touching every construction site.
…y back (#435) `health::auto_create_allowed` fused two very different answers into one bool ("spec opted in" AND "never bootstrapped"), so the mover only ever saw `auto_create == false` and always emitted "spec.create.enabled is false, set it to true" — wrong, and actively misleading, for the reporter whose bucket was deleted while `create.enabled` had been `true` all along. Nothing ever cleared `status.uniqueId`, so the only documented way back was deleting the CR; the reporter deleted the whole namespace. Split the gate into a typed `CreateGate` (`Allowed { via_reinit_ack }` / `DisabledBySpec { also_pinned }` / `PinnedOnceReady`), carry the reason to the mover as `BootstrapRepositoryOp.create_block` + `pinned_unique_id`, and give the once-Ready pin its own sentinel (`RepositoryReinitializeBlocked`) whose message leads with the exact `kubectl annotate` command so a 1024-byte Event note cannot truncate it away. `reinitialize_blocked_message` is the single producer of that text on both the Job path and the controller's in-process bare path, so the two stay byte-identical and the guarded status write stays a no-op. The way back is a uniqueId-valued ack, `kopiur.home-operations.com/allow-reinitialize: <status.uniqueId>`, honored only while it equals the pin — self-expiring, because a successful re-init mints a new id. The never-recreate invariant is unchanged; it just has a door now. Making the ack actually fire needed two more things, since an annotation edit bumps no `metadata.generation`: - `terminal_gate_holds` gains `reinit_requested` as a third opener (the `phase != Ready` guard is load-bearing: on a Ready repository a standing ack must be a complete no-op); - bootstrap Jobs are stamped with the ack they were launched with, and `stale_bootstrap_job` recycles a terminal Job when `live.is_some() && live != stamped`. The `is_some()` half matters — after a successful ack'd re-init the live valid ack is `None` while the good Job is still stamped, and a bare `!=` would recycle it and discard its unconsumed catalog listing. Seed interplay: an ack'd pass computes `seed_armed` as if the pin were absent (so a configured `spec.seed` re-seeds), never resumes, and clears `status.seed` with an explicit RFC 7386 null first — a stale `startedAt` with no `seededAt` would otherwise "resume" a copy into storage that no longer holds anything it touched. Two informational events cover the acks that do NOT re-initialize: `InvalidReinitializeAck` (present, does not match the pin — ignored, fail-safe) and `ReinitializeAckIgnoredRepositoryPresent` (valid, but the repository is healthy).
`health_probe.rs` already proves never-recreate holds under `onFailure: Alert`. This is the other half: under the default `Degrade`, a wiped `ClusterRepository` must park terminal `Failed` with `RepositoryReinitializeBlocked` naming the exact `kubectl annotate` command (and NOT "set spec.create.enabled: true" — it is already true here, which is the reported bug), must reject a mismatched ack with a visible `InvalidReinitializeAck` Warning rather than silence, and must re-initialize on the right one: a rotated `uniqueId`, `BackendReachable=True`, and a backup that succeeds again. Asserts the Event note too, because the note is where truncation could bite: the message leads with the annotate command precisely so the apiserver's 1024-byte clamp cannot remove it. "The ack goes inert once the pin rotates" stays a unit test (`health::create_gate_truth_table`) — e2e'ing it needs a second Degrade→Failed cycle and another ~120s strict-retry holdoff, which blows the per-wait budget. Compile-checked with `cargo check -p kopiur-e2e --features e2e --tests`; the kind suite is not run here.
…443) The whole fan-out state machine, as pure functions over the claim records — no ctx, no kube IO — so it is unit-tested off-cluster before C2 wires it into the reconciler: - aggregate_claims/aggregate_phase/claims_summary fold N claim records into the Restore's own phase and its Ready/Stalled condition. One failed claim stalls the Restore; its siblings keep going. - claim_drive (Drive/Settled/ReArm) keys Settled on the record's TERMINAL phase under the live claimant uid, not on 'bound and no prime': a Failed claim is unbound with its prime still standing, so the latter would re-drive it every 120s forever. - adopt_legacy_claim is the upgrade path — an existing single-PVC populator seeds one claim record from the legacy top-level status, with Completed's three situations told apart by the Ready reason (a RestoreSucceeded legacy is never relabeled TargetAlreadyBound), and an in-flight legacy populate driven under its own Job name so a second mover is never launched into the same prime. - claim_pinned_decision keeps all four arms of pinned_decision, #233 carve-out included. claim_merge_body emits explicit nulls for vanished controller keys and never names a mover-owned key. claim_transition gates Events so the recorder does not just spin one Event's count. - ClaimReason is an enum, not string compares, because one decision is destructive: a hijacked populate's prime holds half-written data and artifacts_reapable refuses it. phase_is_terminal_at_guard: populator Failed is no longer terminal. It now means 'at least one claim failed', and short-circuiting there would freeze healthy siblings and make the standing 're-create the claiming PVC' advice a lie. Terminality moved to RestoreClaimPhase::is_terminal; the hijacked-prime protection the guard used to provide moved to ClaimReason::artifacts_reapable. Also: snapshot_policy::config_identity_for_path resolves a policy's identity against a CALLER-supplied kopia path, mirroring the backup side's IdentityInputs shape so a restore matches what the backup wrote; io::status_merge_patch_is_noop is an RFC-7386-accurate no-op predicate, because the shallow one compares 'claims' whole and could never see a partial per-claim merge as a no-op (a guaranteed write, and reconcile wake-up, every pass times N claims).
`mise run gen` after adding source.fromPolicy.sourcePath and status.claims to the Restore CRD. status.claims renders as an object with additionalProperties (the verificationStamps precedent) — that schema PRUNES unknown keys, so every field the mover writes under claims.<pvc>, observedAt included, is declared on RestoreClaimStatus and asserted by a unit test walking Restore::crd().
The docs said re-creating a once-Ready repository "is always a deliberate human action" without ever saying what that action IS — which is how the reporter ended up deleting their whole namespace. Now there is a procedure. - `docs/repository-health.md` gains a "Deliberately re-initialize a wiped repository" section under the never-auto-recreates warning: the `kubectl annotate` command, what it does (a one-shot first bootstrap; the seed re-arms), and the three properties that make the annotation safe to leave in Git (self-expiring, mismatch ignored loudly, no-op on a healthy repository) — plus a warning that it discards history and to verify the backend is empty first. - `docs/repositories.md` distinguishes the two "no repository at the backend" reasons in the `create.enabled` note and in Safe by construction: they look identical and need opposite fixes. - `docs/troubleshooting.md` gets rows for `RepositoryReinitializeBlocked` and for the "I annotated and nothing happened" case. - `docs/adr/0007` §5 records the amendment: the never-recreate invariant stands, it just has a door now. - `status.uniqueId`'s doc comment (both kinds) now says what its presence MEANS, since it is the flag the whole gate keys on; regenerated CRDs + field reference, and refreshed the repository CRD insta snapshot that pins it. - `KopiurRepositoryNotReady`'s alert description points at the condition message, so an on-call reader knows a `Failed` repository naming `allow-reinitialize` needs a human rather than more waiting.
…436) Both descriptions shared an identical 60-character lead-in ("Grant the operator `secrets` create+patch+delete so ..."), so in the helm-docs-generated README table — where long cells are what the reader scans past — the two rows read as a duplicated entry. The text after the lead-in was always different; nothing was actually duplicated. Each now opens with its own purpose (the `spec.*` field it turns on) and states the grant second. The SECURITY TRADE-OFF sentences are unchanged. Regenerated README.md and values.schema.json (descriptions live in the schema too).
… wires its replacement (#443) Review CRITICAL #1. The guard relaxation shipped one commit ahead of the protection that replaces it, and on this branch that is not a narrow window — it is the steady state of any hijacked populator, every 120s: fail_populate_hijacked writes Failed/PopulateHijacked and LEAVES the prime PVC standing (half-written restore data). With the guard relaxed, the next pass falls through to drive_populator_restore, reads the hijacker-bound claimant as NothingToPopulate, and complete_populator_already_bound deletes the prime — whose PV was never switched to Retain, because the rebind never happened — then patches Completed/TargetAlreadyBound/Ready=True over the loss. Reverted to `RestorePhase::Failed => true`, exactly today's behavior, with a comment naming C2 as the owner of the relaxation and naming the two pure halves that must land in the same commit as it (claim_drive => Settled for a terminal claim, claim_artifacts_reapable refusing PopulateHijacked). Both of those stay here, unused, as C2's inputs. The guard unit test goes back to asserting Failed IS terminal for both dispatch models, and says why. Review IMPORTANT #2 needs no edit as a result: fail_populate_hijacked's doc comment at mod.rs:1322 ("a Failed populator is terminal at the reconcile guard, so nothing reaps it behind the user's back") is true again, verbatim. The forward-looking wording moved to ClaimReason::artifacts_reapable and RestoreClaimPhase::is_terminal, which now say the relaxation is C2's to make rather than describing it as done. Review Minor #8: the two serde_json::to_value(..).unwrap_or_else(|_| json!({})) fallbacks become expect() with a stated reason. Silently degrading to an empty patch would drop a claim's whole state update — the controller would re-drive it forever with nothing in the log — and both types are plain Options with no custom Serialize, so the failure is unreachable rather than handled.
…dy (#435) Review round 1. The critical one (C1) was a hole in exactly the invariant this change exists to protect. `create_enabled` was read straight off `CreateGate`, which says nothing about the phase. On a `Ready` repository carrying a standing valid ack — the state the new docs explicitly invite ("safe to leave in a GitOps manifest", "remove it when convenient") — the gate is `Allowed { via_reinit_ack: true }`, so the ordinary health probe shipped `auto_create: true`. `probe_only` gates only the mover's catalog listing; `bootstrap_init_action` still returns `Create` for NotFound + auto_create. So a wipe under that ack was silently re-created into an empty repository, with no phase transition and no vanish alert — and because a probe never rebinds identity, `status.uniqueId` kept naming the destroyed repository, leaving the ack valid and the repository armed to do it again on every future wipe. `CreateGate::create_block()` is replaced by `health::effective_create(gate, reinit_requested) -> EffectiveCreate { allowed, block }`, which decides the permission and the mover's decline reason together — separating them is what allowed them to disagree. An ack now grants create only once the repository has left `Ready`; while `Ready` it is dormant and reports the same `OnceReadyPinned` block an unacked pinned repository does, so a wipe observed in that window still surfaces the re-initialize hint. All four gate sites use it. The `onFailure: Alert` contract ("never auto-recreates") survives a standing ack for free: Alert keeps the phase `Ready`, so `reinit_requested` is never true. I2: the `InvalidReinitializeAck` Warning is now gated on `phase != Ready` (`health::report_reinit_ack_mismatch`). The instant a successful re-init mints a new id, the ack the user applied stops matching the pin — and reporting that on the healthy repository they just fixed made a permanent Warning the reward for following the documented procedure, contradicting the self-expiring contract. A stale ack on a Ready repository is inert, so it is silent. I3: the ack'd-pass seed decision (arm as-if-unpinned, never resume, clear `status.seed`) moves out of both reconcilers into the pure `repo_seed::reinit_seed_plan`, with the brief's case tested: `startedAt` set, `seededAt` absent, ack valid ⇒ fresh seed, not resume. `clear_status` drives both the RFC-7386 null and the `existing = None` handed to `seed_marker_patch`, so the two can no longer disagree. Docs: `docs/reference/crds/{repository,cluster-repository}.md` (hand-written, and named by the brief) now document uniqueId's pin semantics and gain an Annotations section for `allow-reinitialize`, including the no-`-n` cluster-scoped form. Also fixed a regression the #436 commit introduced and my verification set missed: rewording two `values.yaml` comments shifted every `--8<--` line range below them, which `mise run docs` catches and `mise run test` does not. Minors while in the files: `bootstrap_outcome` binds the failure the guard already proved present instead of carrying a second, unreachable copy of the message; the recycle log line says which evidence went stale (generation vs ack) instead of always claiming "older generation"; and the ` -n <ns>` fragment has one producer, `bootstrap::kubectl_namespace_flag`, shared by both messages.
…the status no-op test (#443) Half of §3.5 landed in C1: the reason consts exist and `plan::ClaimReason` sources every value from them, but `restore/mod.rs` still WROTE the same reasons as inline literals. A reason written in one file and compared through a const in another is exactly the drift the enum was introduced to prevent, just moved one hop — so switch the call sites. Behavior-preserving: every string value is identical. `patch_status_if_changed` now tests for a no-op with `io::status_merge_patch_is_noop` (full RFC-7386, i.e. what the API server actually does) instead of the shallow whole-value-per-key predicate. The two agree on every flat status (already asserted in `io::tests`), and differ only where the deep one is correct: a PARTIAL sub-object patch — the fanned-out populator's `claims: { "<pvc>": {…} }` — and an explicit `null`. Without this the end-of-pass claims patch could never be a no-op and every pass would write. Also carries the C1 review's minors: - `config_identity_for_path` now documents that a selector policy MUST be given the derived path: `None` there takes the `/data` fallback, a path a selector policy was never recorded under. - `claims_summary` counts SETTLED (populated + already-bound) against the total, so a healthy all-no-op restore no longer renders "0/1 populated" beside `Ready=True`; and the unsettled list reads "in flight", not "populating", because it carries `Pending` claims too. - `claim_drive`'s doc no longer says a uid-less record "is DRIVEN" — that is true of the ReArm question only; a uid-less TERMINAL record is `Settled`.
…hout the phase (#435) Re-review round 1 found the adapter had outlived its fix. After C1 every production create site goes through `effective_create(gate, reinit_requested)`, so `auto_create_allowed` had no callers left — but it was still `pub`, still claimed in its doc to be "used at every create site … in both reconcilers", and still answered the phase-blind question. Its doctest asserted `auto_create_allowed(true, Some("abc123"), Some("abc123")) == true` — literally the "Ready + standing ack ⇒ create allowed" verdict C1 exists to forbid, sitting in the public API docs as an example. A helper that can be asked "may create?" without the phase is how the next call site reintroduces C1, so it is gone rather than re-documented. Its Part A prose moves onto `CreateGate`, which now says outright that it is not a create permission on its own, and the doctest moves onto `effective_create` where it can show the property that matters: the same acked gate is `allowed: false` while `Ready` and `allowed: true` once the repository has left it. `auto_create_only_on_first_bootstrap` keeps its name and its whole truth table, now read through `create_gate` → `effective_create` (the only path production has) and extended with the phase dimension. `breaker_verdict`'s doc link re-points to `effective_create`. No behavior change: the deleted fn had no production caller.
…ce path (#443) A populator `Restore` was claimed by exactly one PVC: `claiming_pvc` LISTed the namespace and `.find()`-ed the first hit. With a `pvcSelector` policy and N claimants, claimants 2..N sat `Pending` forever while the Restore reported `Completed` — and the one PVC that DID get filled could be filled with another volume's data, because a `fromPolicy` restore of a selector policy resolved `source_path: None`, which becomes the kopia filter `user@host:` and matches every member path. The reconciler now fans out. `claiming_pvcs` returns every claimant (plus the prime PVCs this Restore owns) from ONE LIST; each claim is driven independently with its own prime PVC, mover `Job` (`{restore}-populate-{fnv8(uid)}`), per-PVC kopia source path and record under `status.claims.<pvc>`; the Restore's own phase is `aggregate_claims` over those records. The pass ends in exactly ONE status patch — phase, observedGeneration, conditions and every claim merge — so the single-conditions-writer rule holds across N claims. Cross-volume fix: `from_policy_identity` derives the path from the CLAIMANT via `expand::restore_source_path` and resolves the identity through `config_identity_for_path`, the same call the BACKUP side makes, so the two strings cannot drift. A policy that names no single volume for a target FAILS CLOSED — `SourceResolution::Ambiguous`, reported as that claim's `Failed/SourcePathAmbiguous` (or, for a direct target, a terminal `Stalled/SourcePathAmbiguous`) — rather than guessing. The same derivation now feeds `snapshot_recorded_source`/`pin_from_recorded_catalog`, so the CR-catalog route cannot match another member's row either. A DIRECT `target.pvc`/`pvcRef` against a selector policy had the identical hazard and takes the same fix. Safety, landed as ONE change (never separable): - `phase_is_terminal_at_guard` relaxes `Failed` to DirectTarget-only, so one failed claim no longer freezes its siblings; - terminality moves to the claim — `claim_drive` returns `Settled` for a terminal record under a live claimant, including `Failed/PopulateHijacked`; - `reap_claim_artifacts` gates on `claim_artifacts_reapable` (an exhaustive `ClaimReason` match, not a string compare), which refuses `PopulateHijacked`, so the prime `fail_populate_hijacked` deliberately leaves standing survives. The guard's doc, the claim-phase doc and `fail_populate_hijacked`'s doc all state that the two halves must stay one change, and the guard unit test asserts the reaper's refusal alongside the relaxation. Also in the fan-out: - WaitForFirstConsumer is PER CLAIM: an unscheduled claimant records `Pending/AwaitingPodSchedule` instead of parking the whole Restore. - `waitTimeout` is per claim (`claim_wait_window`), so a sibling created an hour later gets its own full window rather than the remains of the first one's. - Deleting and re-creating a claiming PVC mints a new uid, which re-arms that claim (`ClaimDrive::ReArm`): the dead claim's prime/Job/PV are reaped and the record starts over. That is the documented retry for a failed claim, and it replaces `reopen_resolution_status`'s explicit-null dance. - Upgrade path: a pre-fan-out status is adopted into `status.claims` for the one claimant the legacy handshake was about (inferred from the uid-keyed `prime-<uid>`, else a lone claimant), and an in-flight legacy populate keeps being driven under its own `{restore}-populate` name — `JobNameReuse::LegacyShared` replaces the `reusable_job_name` bool so a terminating Job under a re-used name can never be read as this run's. - Per-pass caches: one `SnapshotPolicy` GET + one repository resolution + one `StorageClass` GET per class for the whole fan-out, and one cluster-wide PV LIST indexed by claim uid instead of one per claim. An all-settled pass skips the PV LIST entirely. - `run_restore_mover` carries a `RestoreDispatch` (target PVC vs source target vs claim key vs name reuse), and threads `claimKey` into the work spec so N concurrent movers write under `status.claims.<pvc>` instead of clobbering one top-level `status.resolved`. - The mod.rs:1049 lie is fixed: a failed claim now really does say "re-create the claiming PVC to re-arm this claim (other claims continue)". `pinned_decision` sheds its populator-only arms (they live on `claim_pinned_decision`, keyed on the claim's own pin and phase), and the three `phase-allowlist.yaml` entries for constructs this removed are deleted.
The claim/reader machinery lived privately in `repository_lifecycle.rs` and a second, hard-coded copy of `csi_pvc_with_data` lived in `multi_pvc_group.rs`. The fan-out scenario needs both, and a third private copy in a third test binary is exactly how three copies drift. `common` now owns `CSI_STORAGE_CLASS`/`_WFFC`, `BACKUP_LABEL_KEY`, `csi_class_present_or_skip`, `populator_restore_json`, `claiming_pvc_json` and the claim driver. Two shape changes: - `csi_pvc_with_data` takes `label_value` as a PARAMETER. It was hard-coded to the VolumeGroupSnapshot scenario's value, and the scenarios share one cluster and one namespace without deleting their PVCs — so a shared value makes one scenario's `pvcSelector` match another's volumes. - `populate_claim` becomes `populate_claims(.., &[ClaimSpec])`: one populator Restore, N claiming PVCs, one reader pod each asserting that claim's OWN expected content. Before the fan-out a populator filled exactly one claimant, so one claim was all the harness could express — which is precisely why the bug shipped. The single-claim callers keep a thin `populate_claim` wrapper. Also strengthens the #233 "no mover ran" assertion: it looked up the bare `{restore}-populate`, which since #443 is never the name of a per-claim populate Job, so it would have returned `None` no matter how many movers ran. It now matches by prefix.
…443) `crates/e2e/tests/populator_fanout.rs`, four scenarios over two labelled CSI PVCs backed up under one `pvcSelector` policy and then deleted: 1. ONE populator `Restore` re-creates BOTH claiming PVCs, each holding ITS OWN marker, and `status.claims` carries a `Populated` record per claim naming the member path it read. Fails on the pre-fix code twice over — the second claim never binds (its reader pod never schedules), and the first can come back holding the other volume's bytes. 2. A DIRECT `target.pvc` against the same selector policy derives its member from the target's name, and an explicit `fromPolicy.sourcePath` overrides that — the same cross-volume hazard by a different route. 3. A policy whose selector sources disagree on `sourcePathStrategy` FAILS the claim closed with `SourcePathAmbiguous`, stalls the Restore, and names `fromPolicy.sourcePath` in the message. Pre-fix it would have restored whichever member kopia listed newest. 4. One claim fails while its sibling still binds and carries its own data — the regression guard for the `phase_is_terminal_at_guard` relaxation — and deleting and re-creating the failed PVC re-arms that claim. CSI-conditional on purpose: the harness's static hostPath PVCs share one source directory, so a test over them would pass with the bug still present. Uses a THIRD `backup=` label value so the selector can never match another scenario's volumes, `groupBy: None` + `copyMethod: Direct` so no group-snapshot CRDs are involved, and its own repo subpath (registered in `REPO_SUBPATHS` and the node-seed task, in lockstep) so no other scenario's snapshots share its identity. Own CI shard: it deletes and re-creates the PVCs its selector matches.
…443) `docs/restores.md`: - New `#### sourcePath` under `fromPolicy`: the five-rule derivation in order, a `danger` admonition explaining why an absent path is a cross-volume hazard (an empty kopia path matches EVERY member of a selector policy), and a `warning` on the cross-namespace caveat — including the one previously-working mixed-policy shape that changes on upgrade. - The populator section now leads with "one Restore, every claim": per-claim prime/Job/source-path/wait-window/record, a worked `status.claims` output, the aggregate phase, and a note that a failed claim stalls the Restore while its siblings continue — with "delete and re-create THAT PVC" as the re-arm, and the `PopulateHijacked` prime-is-kept exception called out. - "A restore is never held behind a concurrency cap" gains the fan-out cost: N claims means N pooled movers, no per-Restore cap, size the repository cap accordingly. - Deploy-or-restore gains a multi-PVC subsection, snippet-including the three sections of the new example. `docs/troubleshooting.md` (restore/populator rows only): the fan-out diagnosis on the existing "PVC stuck Pending (populator)" row, plus rows for the two pre-0.11 symptoms (only one PVC populated; a PVC holding another volume's data), for a Stalled Restore whose siblings bound fine, for `SourcePathAmbiguous`, for `PopulateHijacked`, and for a cross-namespace `pvcRef` resolving a path the repository never saw. `deploy/examples/44-multi-pvc-deploy-or-restore.yaml` is a 21+ tier example (snippet-included from `docs/restores.md`, deliberately NOT added to the `docs/examples.md` ladder) with section markers for the policy, the Restore and the two claims; `05-deploy-or-restore-gitops.yaml` gains a pointer comment. Type-checked by `crates/api/tests/examples_match_crd_shapes.rs`; `mise run docs` green.
`PassCache::from_policy_referents` tripped `wrong_self_convention` (a `from_*` method taking `&mut self`); it is `policy_referents` now. And `completed_populator_with_ready_reason` went dead when `completed_as_target_already_bound` was retired — the #233 carve-out is keyed on the CLAIM's own `AlreadyBound` phase, not on a derived Ready reason.
…eview round 1) The fan-out itself was sound; the paths AROUND it regressed or misreported. Six findings, each local: CRITICAL — the zero-claimant branch dropped `reap_gone_claims`' result and parked without nulling `status.claims`, so an app torn down with its populator left standing re-reaped and re-published `OrphanedPrimePvcReaped` every 30 s, forever. It is now an end-of-pass writer like the N≥1 branch: `park_unclaimed_populator` builds the park AND the `claims.<gone>: null` merges in ONE body (`plan::awaiting_claim_status`, pure + unit-tested) and writes it through the deep no-op predicate, so the second pass is a server-side no-op. `reap_claim_artifacts` is independently idempotent now — it surveys what actually survives first (`surviving_populate_artifacts`) and returns silently, with no deletes and no Event, when there is nothing left. That also fixes the Warning Event the ordinary "app uninstalled" flow used to publish. IMPORTANT — the Settled sweep reaped a `Failed` claim's mover Job (its pod logs) and its prime 120 s after the record told the user to go read them; restore movers carry no TTL, so pre-#443 both survived until the user acted. The sweep now gates on `plan::settled_artifacts_reapable`, exhaustive over the record's phase: only `Populated`/`AlreadyBound` (the crashed-finalize orphan it exists for) are swept. A failure's artifacts are reclaimed on re-arm or when its record is dropped — which is exactly the documented "re-create the claiming PVC". IMPORTANT — `claim_finalize_rebound` stripped the rebind annotation BEFORE the status write, re-opening (and widening, by the rest of the pass) the crash window the pre-#443 patch-then-finalize ordering closed, while its comment claimed the protection it had removed. The teardown is now carried out of the per-claim driver as `ClaimOutcome::after_patch` and replayed by `run_deferred_finalizers` AFTER the end-of-pass patch lands — the old ordering, with one conditions writer per pass intact. IMPORTANT — legacy adoption with N claimants and exactly one bound (the #443 reporter's own post-upgrade state) adopted nothing, so the already-populated PVC was relabelled `TargetAlreadyBound` — "no restore ran; delete the PVC" — over the volume holding the restored data. The selection rule is now pure and tested (`plan::legacy_claimant`): a surviving uid-keyed `prime-<uid>` first, else exactly one BOUND claimant however many claimants there are. A lone UNBOUND claimant is deliberately NOT adopted — pre-#443 that shape was re-populated, and adopting `Populated` onto it would settle a claim that never got its volume. Adoption is also gated on `plan::is_legacy_populator_status`, so a brand-new Restore's first claim no longer "adopts" the park a zero-claim pass just wrote. IMPORTANT — an adopted `{restore}-populate` Job has no `claimKey`, so its mover pins the TOP-LEVEL `status.resolved`; the finalize read only the claim record and reported `NoSnapshotContinue`/"empty volume" over a real in-flight restore. `plan::claim_finalize_pin` is an exhaustive match on `JobNameReuse` (moved into plan.rs): `LegacyShared` falls back to the top level, `Fresh` never does — C1's `resolved_at` no-fallback rule stays intact so no claim can inherit a sibling's pin. IMPORTANT — `seed_two_member_backups` could not succeed twice in one shard, so whichever of e2e tests 2-4 ran second failed for reasons unrelated to what it tests: children accumulated (no `clear_scenario_leftovers`), the `{name}-seed` Pod was deleted through the PVC API (a no-op, so the next PVC bound EMPTY and every marker assertion failed), and test 2 left an unlabelled `alpha` behind. `clear_scenario_leftovers`/`children_of` are hoisted into `common`, a new `drop_csi_pvc` removes a source PVC *and* its seed Pod and waits, and `csi_pvc_with_data` now clears a stale seed Pod first and waits for the seed to SUCCEED rather than merely for the PVC to bind (Bound is not seeded on an `Immediate` class — a latent flake for every caller). Minors also taken: the reap Event only fires when something existed (#7); adoption gated on a legacy signal (#8); a deliberately-kept prime no longer defeats `all_quiet`, so a hijacked claim stops buying a cluster-wide PV LIST every 600 s (#9, `plan::pass_is_all_quiet`); the `Wedged` arm names its Job (#10); test 4's doc describes the failure it actually induces (#11); `pinned_decision`'s dead `phase` parameter is gone (#12); `reap_gone_claims` documents why the gone-claimant PV is `Leave` and must not be "fixed" into `RestoreOriginalPolicy` (#13); the last `"RestoreSucceeded"` literal is promoted (#14). Complexity held at 29/29 by extracting `surviving_populate_artifacts`, `park_unclaimed_populator`, `run_deferred_finalizers` and the pure `plan::{fanout_status, pass_is_all_quiet}`.
`restore_source_path` evaluated the "no selector sources ⇒ sources[0]'s path" fallback before the exact plain-`pvc:` match. Validation admits N plain `pvc:` sources on one policy, and the backup side fans them out under one user@host with a path each — so for `sources: [pvc: a, pvc: b]`, restoring or populating PVC `b` read `/pvc/a` and filled `b` with `a`'s data. That is the same cross-volume hazard this function exists to close, just without a selector in sight: a wrong-but-non-empty path instead of an empty one. Evaluate the exact match first. This is byte-identical for every single-source shape — a lone plain `pvc:` source whose name equals the target builds the same EffectiveSource (same index, same PvcTargetRef, same override, and `strategy_for` is PvcName for any non-selector source) and therefore the same string; an `nfs` source, a differently-named target and a cross-namespace target all miss the exact match and fall through unchanged. The existing byte-identity test passes as written. A target matching no plain source keeps the documented `sources[0]` fallback rather than failing closed: that is the pre-#443 answer, it is what makes "restore into a differently-named scratch volume" keep working, and rule (5) is reserved for shapes where no path can be derived at all. Said so in the fn doc. Renumbered the rule list in the doc comment and in docs/restores.md, and documented the two upgrade-visible behavior changes.
…me (#443) `adopt_legacy_claims` was gated on `is_legacy_populator_status` alone. The pre-fan-out pass was `ensure_prime_pvc` → `run_restore_mover` (creates `{restore}-populate`) → `patch_status(Restoring)`, and a `helm upgrade` rollout terminates the old operator at an arbitrary instant, not only on a crash — so an in-flight populate is routinely left with the prime AND the Job created and the status still `Pending`/`AwaitingPvcDataSourceRef` with no top-level `resolved`. The gate then read `false`, adoption was skipped, the claim drove `Fresh`, and a SECOND mover was launched under `populate_job_name(uid)` into the very prime `{restore}-populate` was still writing: two kopia restores into one filesystem, which is exactly what `JobNameReuse::LegacyShared` exists to prevent. `status.target.pvcPrime` cannot be the second signal — the legacy driver only ever wrote the `awaiting-claim` sentinel there, never a real prime name, so "set and not the sentinel" is never true for a legacy object. The sound signal is a live `prime-<uid>` this Restore owns for a LIVE claimant: legacy primes carry the same `op=restore-populate` label and ownerRef, so they are already in `Claimants.primes`, and the uid keying distinguishes an in-flight handshake from an orphan the reaper should collect. New pure `legacy_adoption` → `LegacyAdoption` in plan.rs, matched exhaustively at the one call site, with a unit table over terminal-legacy-status, in-flight-prime (with a phase and with none), brand-new, claimant-gone, and already-fanned-out. Over-adopting on the prime signal is harmless: a new-code prime caught in its own window seeds a Pending record with the live uid and no legacy Job, which drives Fresh under its per-uid name as before. `adopt_legacy_claim` now also adopts a status with no phase at all (as Pending — drive it, do not settle it), because that record's `job` field is the whole point of adopting in the upgrade window; a brand-new Restore no longer reaches the function, the gate answers No for it. And it filters the `awaiting-claim` park sentinel out of `record.pvcPrime`, which otherwise sat in a Settled record's status forever; the sentinel is now the named const AWAITING_CLAIM_SENTINEL, shared with `awaiting_claim_status`.
) The previous commit justified putting the exact-name rule ahead of the first-source fallback by claiming that a policy with several plain `pvc:` sources backs each one up under its own path. It does not. `expand_sources` returns `None` unless some source carries a `pvcSelector`, so a selector-free policy is never fanned out: the schedule mints one unpinned child and `effective_source(policy, None)` resolves index 0. `sources: [pvc: a, pvc: b]` captures `/pvc/a` and nothing else — a pre-existing backup-side limitation, tracked separately. The code is unaffected; only the rationale was wrong, and it was wrong in a way that understated the fix. Because only the first plain source is captured, the old fallback returned a path that was real but held ANOTHER volume's data, so restoring into PVC `b` filled it with `a`'s under a green `Completed`. With the exact match first, `b` resolves `/pvc/b` — never written — and the restore fails honestly with `SnapshotNotFound`, or comes up empty under `Continue`. Same for `[nfs, pvc: a]` restoring `a`, which used to restore an NFS export into a PVC. Reworded in docs/restores.md (danger box + upgrade notes), the `restore_source_path` doc comment and its in-body rule-(2) comment, and the matching test comment. No code changed.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
…443) The admission webhook refuses two SnapshotPolicies that resolve to the same identity in one repository; the fail-closed test reused the shared fan-out policy's popfanout@e2e and was denied before it could prove anything. The identity is irrelevant to the assertion (the claim fails closed before any snapshot lookup), so it just needs to be distinct.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
…ses a spec-less base (#443) selector_policy_json handed merge_spec a bare spec object; merge_spec only merges into a full CR's spec, so the overlay was silently dropped and the ambiguous-policy test created a single-source policy under the shared identity — refused by the identity-collision admission guard. Extend the spec object directly, and make merge_spec panic on a spec-less base so this class of silent no-op cannot recur.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
…maintenance, copy-methods)
…e only registers blocks-style admonitions)
…ting started, permissions, rbac, gitops, examples)
…example ladder pointer, duplicate groupBy section, unrendered warning)
…ion, snapshot-replication, server)
…placeholder in scenarios
|
Too many files changed for review (130 files, 100 file limit). Bypass the limit by tagging |
perfectra1n
enabled auto-merge
September 8, 2026 04:50
Merged
doonga
pushed a commit
to greyrock-labs/home-ops
that referenced
this pull request
Sep 8, 2026
….10.7 ➔ 0.10.8) (#149) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | patch | `0.10.7` → `0.10.8` | --- >⚠️ **Warning** > > Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/40) for more information. --- ### Release Notes <details> <summary>home-operations/kopiur (ghcr.io/home-operations/charts/kopiur)</summary> ### [`v0.10.8`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0108-2026-09-08) [Compare Source](home-operations/kopiur@0.10.7...0.10.8) ##### Bug Fixes - **container:** update image busybox ([`9532d8c`](home-operations/kopiur@9532d8c) → [`dc2d74b`](home-operations/kopiur@dc2d74b)) ([#​439](home-operations/kopiur#439)) ([0bad734](home-operations/kopiur@0bad734)) - re-initialize ack for wiped repositories ([#​435](home-operations/kopiur#435)), populator fan-out with per-PVC source paths ([#​443](home-operations/kopiur#443)), helm README ([#​436](home-operations/kopiur#436)) ([#​449](home-operations/kopiur#449)) ([fdfa9a6](home-operations/kopiur@fdfa9a6)) - **rust:** update crate rustls (0.23.43 → 0.23.44) ([#​448](home-operations/kopiur#448)) ([b9e10a6](home-operations/kopiur@b9e10a6)) ##### Miscellaneous Chores - **github-action:** update action actions/deploy-pages (v5.0.0 → v5.0.1) ([#​444](home-operations/kopiur#444)) ([f601ebd](home-operations/kopiur@f601ebd)) - **github-action:** update action helm/kind-action (v1.14.0 → v1.15.0) ([#​446](home-operations/kopiur#446)) ([94ab9de](home-operations/kopiur@94ab9de)) - **mise:** update mise tools ([#​399](home-operations/kopiur#399)) ([ffae98a](home-operations/kopiur@ffae98a)) - **mise:** update tool aqua:astral-sh/uv (0.12.7 → 0.12.8) ([#​442](home-operations/kopiur#442)) ([8954063](home-operations/kopiur@8954063)) - **mise:** update tool aqua:astral-sh/uv (0.12.8 → 0.12.9) ([#​445](home-operations/kopiur#445)) ([5a88ac6](home-operations/kopiur@5a88ac6)) - **mise:** update tool aqua:astral-sh/uv (0.12.9 → 0.12.10) ([#​450](home-operations/kopiur#450)) ([c32246a](home-operations/kopiur@c32246a)) - **mise:** update tool zizmor (1.29.0 → 1.30.0) ([#​441](home-operations/kopiur#441)) ([908a427](home-operations/kopiur@908a427)) </details> --- ### Configuration 📅 **Schedule**: (in timezone America/New_York) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42Ni4xIiwidXBkYXRlZEluVmVyIjoiNDQuNjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUvY29udGFpbmVyIiwidHlwZS9wYXRjaCJdfQ==--> Reviewed-on: https://git.greyrock.io/todd/home-ops/pulls/149
gabrielcosi
pushed a commit
to gabrielcosi/home-ops
that referenced
this pull request
Sep 8, 2026
….10.7 ➔ 0.10.8) (#688) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | patch | `0.10.7` → `0.10.8` | --- ### Release Notes <details> <summary>home-operations/kopiur (ghcr.io/home-operations/charts/kopiur)</summary> ### [`v0.10.8`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0108-2026-09-08) [Compare Source](home-operations/kopiur@0.10.7...0.10.8) ##### Bug Fixes - **container:** update image busybox ([`9532d8c`](home-operations/kopiur@9532d8c) → [`dc2d74b`](home-operations/kopiur@dc2d74b)) ([#​439](home-operations/kopiur#439)) ([0bad734](home-operations/kopiur@0bad734)) - re-initialize ack for wiped repositories ([#​435](home-operations/kopiur#435)), populator fan-out with per-PVC source paths ([#​443](home-operations/kopiur#443)), helm README ([#​436](home-operations/kopiur#436)) ([#​449](home-operations/kopiur#449)) ([fdfa9a6](home-operations/kopiur@fdfa9a6)) - **rust:** update crate rustls (0.23.43 → 0.23.44) ([#​448](home-operations/kopiur#448)) ([b9e10a6](home-operations/kopiur@b9e10a6)) ##### Miscellaneous Chores - **github-action:** update action actions/deploy-pages (v5.0.0 → v5.0.1) ([#​444](home-operations/kopiur#444)) ([f601ebd](home-operations/kopiur@f601ebd)) - **github-action:** update action helm/kind-action (v1.14.0 → v1.15.0) ([#​446](home-operations/kopiur#446)) ([94ab9de](home-operations/kopiur@94ab9de)) - **mise:** update mise tools ([#​399](home-operations/kopiur#399)) ([ffae98a](home-operations/kopiur@ffae98a)) - **mise:** update tool aqua:astral-sh/uv (0.12.7 → 0.12.8) ([#​442](home-operations/kopiur#442)) ([8954063](home-operations/kopiur@8954063)) - **mise:** update tool aqua:astral-sh/uv (0.12.8 → 0.12.9) ([#​445](home-operations/kopiur#445)) ([5a88ac6](home-operations/kopiur@5a88ac6)) - **mise:** update tool aqua:astral-sh/uv (0.12.9 → 0.12.10) ([#​450](home-operations/kopiur#450)) ([c32246a](home-operations/kopiur@c32246a)) - **mise:** update tool zizmor (1.29.0 → 1.30.0) ([#​441](home-operations/kopiur#441)) ([908a427](home-operations/kopiur@908a427)) </details> --- ### Configuration 📅 **Schedule**: (in timezone Europe/Berlin) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42NS4wIiwidXBkYXRlZEluVmVyIjoiNDQuNjUuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUvY29udGFpbmVyIiwidHlwZS9wYXRjaCJdfQ==--> Reviewed-on: https://git.xcd.dev/gabrielcosi/home-ops/pulls/688
eleboucher
pushed a commit
to eleboucher/homelab
that referenced
this pull request
Sep 10, 2026
….10.7 ➔ 0.10.8) (#2046) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | patch | `0.10.7` → `0.10.8` | --- >⚠️ **Warning** > > Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/567) for more information. --- ### Release Notes <details> <summary>home-operations/kopiur (ghcr.io/home-operations/charts/kopiur)</summary> ### [`v0.10.8`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0108-2026-09-08) [Compare Source](home-operations/kopiur@0.10.7...0.10.8) ##### Bug Fixes - **container:** update image busybox ([`9532d8c`](home-operations/kopiur@9532d8c) → [`dc2d74b`](home-operations/kopiur@dc2d74b)) ([#​439](home-operations/kopiur#439)) ([0bad734](home-operations/kopiur@0bad734)) - re-initialize ack for wiped repositories ([#​435](home-operations/kopiur#435)), populator fan-out with per-PVC source paths ([#​443](home-operations/kopiur#443)), helm README ([#​436](home-operations/kopiur#436)) ([#​449](home-operations/kopiur#449)) ([fdfa9a6](home-operations/kopiur@fdfa9a6)) - **rust:** update crate rustls (0.23.43 → 0.23.44) ([#​448](home-operations/kopiur#448)) ([b9e10a6](home-operations/kopiur@b9e10a6)) ##### Miscellaneous Chores - **github-action:** update action actions/deploy-pages (v5.0.0 → v5.0.1) ([#​444](home-operations/kopiur#444)) ([f601ebd](home-operations/kopiur@f601ebd)) - **github-action:** update action helm/kind-action (v1.14.0 → v1.15.0) ([#​446](home-operations/kopiur#446)) ([94ab9de](home-operations/kopiur@94ab9de)) - **mise:** update mise tools ([#​399](home-operations/kopiur#399)) ([ffae98a](home-operations/kopiur@ffae98a)) - **mise:** update tool aqua:astral-sh/uv (0.12.7 → 0.12.8) ([#​442](home-operations/kopiur#442)) ([8954063](home-operations/kopiur@8954063)) - **mise:** update tool aqua:astral-sh/uv (0.12.8 → 0.12.9) ([#​445](home-operations/kopiur#445)) ([5a88ac6](home-operations/kopiur@5a88ac6)) - **mise:** update tool aqua:astral-sh/uv (0.12.9 → 0.12.10) ([#​450](home-operations/kopiur#450)) ([c32246a](home-operations/kopiur@c32246a)) - **mise:** update tool zizmor (1.29.0 → 1.30.0) ([#​441](home-operations/kopiur#441)) ([908a427](home-operations/kopiur@908a427)) </details> --- ### Configuration 📅 **Schedule**: (in timezone Europe/Paris) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC42NS41IiwidXBkYXRlZEluVmVyIjoiNDQuNjUuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUvY29udGFpbmVyIiwidHlwZS9wYXRjaCJdfQ==--> Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/2046
GiorgioAresu
pushed a commit
to GiorgioAresu/home-ops
that referenced
this pull request
Sep 13, 2026
….10.0 ➔ 0.10.8) (#2042) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | patch | `0.10.0` → `0.10.8` | --- >⚠️ **Warning** > > Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/6) for more information. --- ### Release Notes <details> <summary>home-operations/kopiur (ghcr.io/home-operations/charts/kopiur)</summary> ### [`v0.10.8`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0108-2026-09-08) [Compare Source](home-operations/kopiur@0.10.7...0.10.8) ##### Bug Fixes - **container:** update image busybox ([`9532d8c`](home-operations/kopiur@9532d8c) → [`dc2d74b`](home-operations/kopiur@dc2d74b)) ([#​439](home-operations/kopiur#439)) ([0bad734](home-operations/kopiur@0bad734)) - re-initialize ack for wiped repositories ([#​435](home-operations/kopiur#435)), populator fan-out with per-PVC source paths ([#​443](home-operations/kopiur#443)), helm README ([#​436](home-operations/kopiur#436)) ([#​449](home-operations/kopiur#449)) ([fdfa9a6](home-operations/kopiur@fdfa9a6)) - **rust:** update crate rustls (0.23.43 → 0.23.44) ([#​448](home-operations/kopiur#448)) ([b9e10a6](home-operations/kopiur@b9e10a6)) ##### Miscellaneous Chores - **github-action:** update action actions/deploy-pages (v5.0.0 → v5.0.1) ([#​444](home-operations/kopiur#444)) ([f601ebd](home-operations/kopiur@f601ebd)) - **github-action:** update action helm/kind-action (v1.14.0 → v1.15.0) ([#​446](home-operations/kopiur#446)) ([94ab9de](home-operations/kopiur@94ab9de)) - **mise:** update mise tools ([#​399](home-operations/kopiur#399)) ([ffae98a](home-operations/kopiur@ffae98a)) - **mise:** update tool aqua:astral-sh/uv (0.12.7 → 0.12.8) ([#​442](home-operations/kopiur#442)) ([8954063](home-operations/kopiur@8954063)) - **mise:** update tool aqua:astral-sh/uv (0.12.8 → 0.12.9) ([#​445](home-operations/kopiur#445)) ([5a88ac6](home-operations/kopiur@5a88ac6)) - **mise:** update tool aqua:astral-sh/uv (0.12.9 → 0.12.10) ([#​450](home-operations/kopiur#450)) ([c32246a](home-operations/kopiur@c32246a)) - **mise:** update tool zizmor (1.29.0 → 1.30.0) ([#​441](home-operations/kopiur#441)) ([908a427](home-operations/kopiur@908a427)) ### [`v0.10.7`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0107-2026-09-02) [Compare Source](home-operations/kopiur@0.10.6...0.10.7) ##### Features - per-repository mover-Job concurrency limits + jitter hardening ([#​437](home-operations/kopiur#437)) ([1529aa9](home-operations/kopiur@1529aa9)) ### [`v0.10.6`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0106-2026-09-01) [Compare Source](home-operations/kopiur@0.10.5...0.10.6) ##### Features - **errors:** clarify user-facing Kubernetes error/warning messages ([#​431](home-operations/kopiur#431)) ([099d0a1](home-operations/kopiur@099d0a1)) ##### Bug Fixes - **controller:** remove the NoAdoptableHistory warning event ([#​429](home-operations/kopiur#429)) ([a3dccb4](home-operations/kopiur@a3dccb4)) - kopia UI server never received the backend auth Secret — split-secret creds, per-secret mirrors, server identity ([#​416](home-operations/kopiur#416)) ([#​424](home-operations/kopiur#424)) ([ab1327a](home-operations/kopiur@ab1327a)) - **rust:** update crate rcgen (0.14.9 → 0.14.10) ([#​423](home-operations/kopiur#423)) ([48c10a3](home-operations/kopiur@48c10a3)) ##### Miscellaneous Chores - **github-action:** update action anchore/sbom-action (v0.24.0 → v0.24.1) ([#​427](home-operations/kopiur#427)) ([4aee76d](home-operations/kopiur@4aee76d)) - **github-action:** update action anchore/sbom-action (v0.24.1 → v0.24.2) ([#​432](home-operations/kopiur#432)) ([0d029d4](home-operations/kopiur@0d029d4)) - **mise:** update tool aqua:astral-sh/uv (0.12.5 → 0.12.6) ([#​422](home-operations/kopiur#422)) ([d06d57c](home-operations/kopiur@d06d57c)) - **mise:** update tool aqua:astral-sh/uv (0.12.6 → 0.12.7) ([#​428](home-operations/kopiur#428)) ([36f8f56](home-operations/kopiur@36f8f56)) - **mise:** update tool aqua:dadav/helm-schema (0.23.4 → 0.23.5) ([#​412](home-operations/kopiur#412)) ([c1ad2a1](home-operations/kopiur@c1ad2a1)) - **mise:** update tool kubectl (1.36.4 → 1.37.0) ([#​426](home-operations/kopiur#426)) ([08eb492](home-operations/kopiur@08eb492)) - **mise:** update tool lefthook (2.1.11 → 2.1.12) ([#​430](home-operations/kopiur#430)) ([21312e5](home-operations/kopiur@21312e5)) - **mise:** update tool node (24.19.0 → v24.20.0) ([#​425](home-operations/kopiur#425)) ([9e4ffb8](home-operations/kopiur@9e4ffb8)) ### [`v0.10.5`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0105-2026-08-28) [Compare Source](home-operations/kopiur@0.10.4...0.10.5) ##### Bug Fixes - deadline-killed bootstrap classification, maintenance gate exemption, relaunch backoff + self-healing escalation ([#​417](home-operations/kopiur#417)) ([ff8b396](home-operations/kopiur@ff8b396)) - manualRun explicit-null completedAt, tri-state Restore gate, throttle honored in every mover flow ([#​394](home-operations/kopiur#394), [#​393](home-operations/kopiur#393), [#​374](home-operations/kopiur#374)) ([#​411](home-operations/kopiur#411)) ([8f0399f](home-operations/kopiur@8f0399f)) ##### Miscellaneous Chores - **github-action:** update action jdx/mise-action (v4.2.5 → v4.3.0) ([#​419](home-operations/kopiur#419)) ([6234791](home-operations/kopiur@6234791)) ### [`v0.10.4`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0104-2026-08-24) [Compare Source](home-operations/kopiur@0.10.3...0.10.4) ##### Bug Fixes - **chart:** give flowSchema its doc comment and timings a clean schema entry ([#​397](home-operations/kopiur#397)) ([7c8dd3f](home-operations/kopiur@7c8dd3f)) - **mover:** read status.resolved via the /status subresource ([#​408](home-operations/kopiur#408)) ([06fc2f9](home-operations/kopiur@06fc2f9)) ##### Miscellaneous Chores - **github-action:** update action docker/github-builder (v1.16.0 → v1.17.0) ([#​407](home-operations/kopiur#407)) ([b6d7d04](home-operations/kopiur@b6d7d04)) - **mise:** update mise tools ([#​400](home-operations/kopiur#400)) ([b1a83f8](home-operations/kopiur@b1a83f8)) - **mise:** update tool kubectl (1.36.3 → 1.36.4) ([#​405](home-operations/kopiur#405)) ([274e3d6](home-operations/kopiur@274e3d6)) - **mise:** update tool lefthook (2.1.10 → 2.1.11) ([#​406](home-operations/kopiur#406)) ([2ba700d](home-operations/kopiur@2ba700d)) - **mise:** update tool rust (1.97.1 → 1.98.0) ([#​403](home-operations/kopiur#403)) ([c655005](home-operations/kopiur@c655005)) - **mise:** update tool yq (4.53.3 → 4.53.4) ([#​402](home-operations/kopiur#402)) ([d1ed8a7](home-operations/kopiur@d1ed8a7)) - **mise:** update tool yq (4.53.4 → 4.53.6) ([#​404](home-operations/kopiur#404)) ([dd5878c](home-operations/kopiur@dd5878c)) ### [`v0.10.3`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0103-2026-08-18) [Compare Source](home-operations/kopiur@0.10.2...0.10.3) ##### Features - **repository:** seed a fresh repository from its replica — DR for replicated repositories ([#​380](home-operations/kopiur#380)) ([#​396](home-operations/kopiur#396)) ([74665dc](home-operations/kopiur@74665dc)) ##### Bug Fixes - **rust:** update crate cel (0.14.2 → 0.14.3) ([#​387](home-operations/kopiur#387)) ([bf9aa0c](home-operations/kopiur@bf9aa0c)) - **rust:** update crate http-body-util (0.1.4 → 0.1.5) ([#​381](home-operations/kopiur#381)) ([6354e17](home-operations/kopiur@6354e17)) ##### Miscellaneous Chores - **github-action:** update action jdx/mise-action (v4.2.4 → v4.2.5) ([#​388](home-operations/kopiur#388)) ([1040c9e](home-operations/kopiur@1040c9e)) - **mise:** update tool aqua:astral-sh/uv (0.12.3 → 0.12.4) ([#​390](home-operations/kopiur#390)) ([bf3916c](home-operations/kopiur@bf3916c)) - **mise:** update tool aqua:astral-sh/uv (0.12.4 → 0.12.5) ([#​395](home-operations/kopiur#395)) ([b919878](home-operations/kopiur@b919878)) - **mise:** update tool helm (4.2.3 → 4.2.4) ([#​389](home-operations/kopiur#389)) ([5b134fa](home-operations/kopiur@5b134fa)) - **mise:** Update tool oxfmt (0.62.0 → 0.63.0) ([#​386](home-operations/kopiur#386)) ([65db237](home-operations/kopiur@65db237)) ### [`v0.10.2`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0102-2026-08-13) [Compare Source](home-operations/kopiur@0.10.1...0.10.2) ##### Performance Improvements - **controller:** store-backed reads, LIST dedup, bounded PVC retries, client-side request metrics ([#​382](home-operations/kopiur#382)) ([#​383](home-operations/kopiur#383)) ([2b07022](home-operations/kopiur@2b07022)) ### [`v0.10.1`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0101-2026-08-11) [Compare Source](home-operations/kopiur@0.10.0...0.10.1) ##### Bug Fixes - **mise:** something about mise warnings ([#​373](home-operations/kopiur#373)) ([fde8073](home-operations/kopiur@fde8073)) - **rust:** update crate cel (0.14.1 → 0.14.2) ([#​379](home-operations/kopiur#379)) ([4a60221](home-operations/kopiur@4a60221)) - **rust:** update crate futures (0.3.33 → 0.3.34) ([#​378](home-operations/kopiur#378)) ([ff657b8](home-operations/kopiur@ff657b8)) - **rust:** update crate rcgen (0.14.8 → 0.14.9) ([#​377](home-operations/kopiur#377)) ([e3b3b3f](home-operations/kopiur@e3b3b3f)) ##### Miscellaneous Chores - **mise:** Update tool aqua:astral-sh/uv (0.12.2 → 0.12.3) ([#​375](home-operations/kopiur#375)) ([952af6d](home-operations/kopiur@952af6d)) </details> --- ### Configuration 📅 **Schedule**: (in timezone Europe/Rome) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xNy4xIiwidXBkYXRlZEluVmVyIjoiNDQuMTcuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUvY29udGFpbmVyIiwidHlwZS9wYXRjaCJdfQ==--> Reviewed-on: https://git.aresu.eu/GiorgioAresu/home-ops/pulls/2042
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #435, #436, #443.
#435 — a wiped backend now tells the truth, and there is a way back
Once a
Repository/ClusterRepositoryhas beenReady, kopiur pinsstatus.uniqueIdand will never silently create an empty repository over the backend. That invariant stays. What changed:Failedwith reasonRepositoryReinitializeBlockedinstead of the misleading "spec.create.enabled is false" (which still appears only when create really is disabled on a never-bootstrapped repository).kubectl annotate <kind> <name> kopiur.home-operations.com/allow-reinitialize=<status.uniqueId>re-runs a strict bootstrap that may create. The value must equal the pinned id, so the ack goes inert the moment the new id lands; it is a no-op on aReadyrepository (no phase flip, no identity rebind, no create), and a mismatched value raises aInvalidReinitializeAckWarning naming the expected value. Thekubectl annotatecommand is the first thing in the condition message and Event note.CreateGate/effective_create(no phase-blind "may create?" helper exists any more), the ack is stamped on the bootstrap Job so a terminal Job launched before the ack is recycled immediately,terminal_gate_holdsopens on the ack,status.seedis cleared so a configured seed re-seeds rather than "resumes".repository-health.mdgains "Deliberately re-initialize a wiped repository"; CRD reference, troubleshooting, ADR-0007 amendment,KopiurRepositoryNotReadyalert text.degrade_mode_wipe_escalates_to_reinitialize_blocked_and_ack_recreates.#436 — Helm README
The two
features.*.enabledrows shared a 60-char lead-in and read as duplicates. Reworded invalues.yaml, README +values.schema.jsonregenerated (snippet line ranges indocs/configuration.mdupdated to match).#443 — a populator
Restorepopulates every claiming PVC, each from its own snapshottarget.populatorRestore now fills every PVC whosedataSourceRefnames it. Per-claim state lives instatus.claims.<pvcName>(RestoreClaimPhase: Pending/Populating/Rebinding/Populated/AlreadyBound/Failed); the Restore's own phase/Ready condition is the aggregate. A failed claim stalls the Restore but siblings continue; recreating the failed PVC re-arms it. WaitForFirstConsumer is per claim. Per-claim Jobs (<restore>-populate-<hash>), one PVC LIST + one PV LIST per pass, one status patch per pass (claims-aware no-op check), events on transitions only.fromPolicyrestore against apvcSelectorpolicy previously resolved a pathless identity, so the mover matched the newest snapshot across every member PVC and could fill a PVC with another PVC's data. The source path is now derived per target/claim from the PVC name via the policy'ssourcePathStrategy; a newspec.source.fromPolicy.sourcePathoverride exists; ambiguous shapes fail closed withSourcePathAmbiguous. Same fix on the directtarget.pvc/pvcRefpath.<name>-populateJob is driven to completion (never a second mover into the same prime), and a hijacked populate never loses its prime.restores.mdpopulator +fromPolicy.sourcePathsections, troubleshooting rows, new exampledeploy/examples/44-multi-pvc-deploy-or-restore.yaml.populator_fanout.rs(cross-volume regression guard: each PVC gets its own marker; direct target with/without override; fail-closed; sibling isolation).Known limitation surfaced while fixing #443 (pre-existing, not changed here)
A policy with several plain
pvc:sources and no selector is admitted by validation but onlysources[0]is ever backed up. The restore side now resolves the exact-name source's own path (so such a restore surfacesSnapshotNotFoundinstead of silently restoring the first PVC's data); the backup-side limitation is documented inrestores.mdand should get its own issue.Verification
mise run fmt-check / clippy / test / gen-check / helm-docs-check / helm-test / docs / complexity-check,cargo xtask check-phases / check-wiringall green. Kind e2e: see the CI run; locally the two NFS tests fail on this box as always.