Skip to content

fix: re-initialize ack for wiped repositories (#435), populator fan-out with per-PVC source paths (#443), helm README (#436) - #449

Merged
perfectra1n merged 76 commits into
mainfrom
fix/issues-435-436-443
Sep 8, 2026
Merged

perfectra1n merged 76 commits into
mainfrom
fix/issues-435-436-443

Conversation

@perfectra1n

Copy link
Copy Markdown
Collaborator

Fixes #435, #436, #443.

#435 — a wiped backend now tells the truth, and there is a way back

Once a Repository/ClusterRepository has been Ready, kopiur pins status.uniqueId and will never silently create an empty repository over the backend. That invariant stays. What changed:

  • Accurate reason. The mover's create decision now carries why create is blocked. A once-Ready repository whose backend went missing parks Failed with reason RepositoryReinitializeBlocked instead of the misleading "spec.create.enabled is false" (which still appears only when create really is disabled on a never-bootstrapped repository).
  • Deliberate re-initialize ack. 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 a Ready repository (no phase flip, no identity rebind, no create), and a mismatched value raises a InvalidReinitializeAck Warning naming the expected value. The kubectl annotate command is the first thing in the condition message and Event note.
  • Plumbing: 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_holds opens on the ack, status.seed is cleared so a configured seed re-seeds rather than "resumes".
  • Docs: repository-health.md gains "Deliberately re-initialize a wiped repository"; CRD reference, troubleshooting, ADR-0007 amendment, KopiurRepositoryNotReady alert text.
  • Tests: unit truth tables for the gate/ack/seed/recycle predicates; new e2e degrade_mode_wipe_escalates_to_reinitialize_blocked_and_ack_recreates.

#436 — Helm README

The two features.*.enabled rows shared a 60-char lead-in and read as duplicates. Reworded in values.yaml, README + values.schema.json regenerated (snippet line ranges in docs/configuration.md updated to match).

#443 — a populator Restore populates every claiming PVC, each from its own snapshot

  • Fan-out. One target.populator Restore now fills every PVC whose dataSourceRef names it. Per-claim state lives in status.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.
  • Cross-volume hazard closed. A fromPolicy restore against a pvcSelector policy 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's sourcePathStrategy; a new spec.source.fromPolicy.sourcePath override exists; ambiguous shapes fail closed with SourcePathAmbiguous. Same fix on the direct target.pvc/pvcRef path.
  • Upgrade path. Existing single-PVC populator Restores keep working with no spec change; legacy status is adopted into a claim record, an in-flight legacy <name>-populate Job is driven to completion (never a second mover into the same prime), and a hijacked populate never loses its prime.
  • Docs: restores.md populator + fromPolicy.sourcePath sections, troubleshooting rows, new example deploy/examples/44-multi-pvc-deploy-or-restore.yaml.
  • Tests: per-claim decision tables, source-path golden cases (incl. byte-identity with the backup side), mover claim-scoped status shapes, new e2e 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 only sources[0] is ever backed up. The restore side now resolves the exact-name source's own path (so such a restore surfaces SnapshotNotFound instead of silently restoring the first PVC's data); the backup-side limitation is documented in restores.md and 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-wiring all green. Kind e2e: see the CI run; locally the two NFS tests fail on this box as always.

…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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

…ting started, permissions, rbac, gitops, examples)
…example ladder pointer, duplicate groupBy section, unrendered warning)
@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown

Too many files changed for review (130 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@perfectra1n
perfectra1n added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit fdfa9a6 Sep 8, 2026
39 checks passed
@perfectra1n
perfectra1n deleted the fix/issues-435-436-443 branch September 8, 2026 05:41
@sticky-gecko sticky-gecko Bot mentioned this pull request Sep 8, 2026
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)) ([#&#8203;439](home-operations/kopiur#439)) ([0bad734](home-operations/kopiur@0bad734))
- re-initialize ack for wiped repositories ([#&#8203;435](home-operations/kopiur#435)), populator fan-out with per-PVC source paths ([#&#8203;443](home-operations/kopiur#443)), helm README ([#&#8203;436](home-operations/kopiur#436)) ([#&#8203;449](home-operations/kopiur#449)) ([fdfa9a6](home-operations/kopiur@fdfa9a6))
- **rust:** update crate rustls (0.23.43 → 0.23.44) ([#&#8203;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) ([#&#8203;444](home-operations/kopiur#444)) ([f601ebd](home-operations/kopiur@f601ebd))
- **github-action:** update action helm/kind-action (v1.14.0 → v1.15.0) ([#&#8203;446](home-operations/kopiur#446)) ([94ab9de](home-operations/kopiur@94ab9de))
- **mise:** update mise tools ([#&#8203;399](home-operations/kopiur#399)) ([ffae98a](home-operations/kopiur@ffae98a))
- **mise:** update tool aqua:astral-sh/uv (0.12.7 → 0.12.8) ([#&#8203;442](home-operations/kopiur#442)) ([8954063](home-operations/kopiur@8954063))
- **mise:** update tool aqua:astral-sh/uv (0.12.8 → 0.12.9) ([#&#8203;445](home-operations/kopiur#445)) ([5a88ac6](home-operations/kopiur@5a88ac6))
- **mise:** update tool aqua:astral-sh/uv (0.12.9 → 0.12.10) ([#&#8203;450](home-operations/kopiur#450)) ([c32246a](home-operations/kopiur@c32246a))
- **mise:** update tool zizmor (1.29.0 → 1.30.0) ([#&#8203;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)) ([#&#8203;439](home-operations/kopiur#439)) ([0bad734](home-operations/kopiur@0bad734))
- re-initialize ack for wiped repositories ([#&#8203;435](home-operations/kopiur#435)), populator fan-out with per-PVC source paths ([#&#8203;443](home-operations/kopiur#443)), helm README ([#&#8203;436](home-operations/kopiur#436)) ([#&#8203;449](home-operations/kopiur#449)) ([fdfa9a6](home-operations/kopiur@fdfa9a6))
- **rust:** update crate rustls (0.23.43 → 0.23.44) ([#&#8203;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) ([#&#8203;444](home-operations/kopiur#444)) ([f601ebd](home-operations/kopiur@f601ebd))
- **github-action:** update action helm/kind-action (v1.14.0 → v1.15.0) ([#&#8203;446](home-operations/kopiur#446)) ([94ab9de](home-operations/kopiur@94ab9de))
- **mise:** update mise tools ([#&#8203;399](home-operations/kopiur#399)) ([ffae98a](home-operations/kopiur@ffae98a))
- **mise:** update tool aqua:astral-sh/uv (0.12.7 → 0.12.8) ([#&#8203;442](home-operations/kopiur#442)) ([8954063](home-operations/kopiur@8954063))
- **mise:** update tool aqua:astral-sh/uv (0.12.8 → 0.12.9) ([#&#8203;445](home-operations/kopiur#445)) ([5a88ac6](home-operations/kopiur@5a88ac6))
- **mise:** update tool aqua:astral-sh/uv (0.12.9 → 0.12.10) ([#&#8203;450](home-operations/kopiur#450)) ([c32246a](home-operations/kopiur@c32246a))
- **mise:** update tool zizmor (1.29.0 → 1.30.0) ([#&#8203;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)) ([#&#8203;439](home-operations/kopiur#439)) ([0bad734](home-operations/kopiur@0bad734))
- re-initialize ack for wiped repositories ([#&#8203;435](home-operations/kopiur#435)), populator fan-out with per-PVC source paths ([#&#8203;443](home-operations/kopiur#443)), helm README ([#&#8203;436](home-operations/kopiur#436)) ([#&#8203;449](home-operations/kopiur#449)) ([fdfa9a6](home-operations/kopiur@fdfa9a6))
- **rust:** update crate rustls (0.23.43 → 0.23.44) ([#&#8203;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) ([#&#8203;444](home-operations/kopiur#444)) ([f601ebd](home-operations/kopiur@f601ebd))
- **github-action:** update action helm/kind-action (v1.14.0 → v1.15.0) ([#&#8203;446](home-operations/kopiur#446)) ([94ab9de](home-operations/kopiur@94ab9de))
- **mise:** update mise tools ([#&#8203;399](home-operations/kopiur#399)) ([ffae98a](home-operations/kopiur@ffae98a))
- **mise:** update tool aqua:astral-sh/uv (0.12.7 → 0.12.8) ([#&#8203;442](home-operations/kopiur#442)) ([8954063](home-operations/kopiur@8954063))
- **mise:** update tool aqua:astral-sh/uv (0.12.8 → 0.12.9) ([#&#8203;445](home-operations/kopiur#445)) ([5a88ac6](home-operations/kopiur@5a88ac6))
- **mise:** update tool aqua:astral-sh/uv (0.12.9 → 0.12.10) ([#&#8203;450](home-operations/kopiur#450)) ([c32246a](home-operations/kopiur@c32246a))
- **mise:** update tool zizmor (1.29.0 → 1.30.0) ([#&#8203;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)) ([#&#8203;439](home-operations/kopiur#439)) ([0bad734](home-operations/kopiur@0bad734))
- re-initialize ack for wiped repositories ([#&#8203;435](home-operations/kopiur#435)), populator fan-out with per-PVC source paths ([#&#8203;443](home-operations/kopiur#443)), helm README ([#&#8203;436](home-operations/kopiur#436)) ([#&#8203;449](home-operations/kopiur#449)) ([fdfa9a6](home-operations/kopiur@fdfa9a6))
- **rust:** update crate rustls (0.23.43 → 0.23.44) ([#&#8203;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) ([#&#8203;444](home-operations/kopiur#444)) ([f601ebd](home-operations/kopiur@f601ebd))
- **github-action:** update action helm/kind-action (v1.14.0 → v1.15.0) ([#&#8203;446](home-operations/kopiur#446)) ([94ab9de](home-operations/kopiur@94ab9de))
- **mise:** update mise tools ([#&#8203;399](home-operations/kopiur#399)) ([ffae98a](home-operations/kopiur@ffae98a))
- **mise:** update tool aqua:astral-sh/uv (0.12.7 → 0.12.8) ([#&#8203;442](home-operations/kopiur#442)) ([8954063](home-operations/kopiur@8954063))
- **mise:** update tool aqua:astral-sh/uv (0.12.8 → 0.12.9) ([#&#8203;445](home-operations/kopiur#445)) ([5a88ac6](home-operations/kopiur@5a88ac6))
- **mise:** update tool aqua:astral-sh/uv (0.12.9 → 0.12.10) ([#&#8203;450](home-operations/kopiur#450)) ([c32246a](home-operations/kopiur@c32246a))
- **mise:** update tool zizmor (1.29.0 → 1.30.0) ([#&#8203;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 ([#&#8203;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 ([#&#8203;431](home-operations/kopiur#431)) ([099d0a1](home-operations/kopiur@099d0a1))

##### Bug Fixes

- **controller:** remove the NoAdoptableHistory warning event ([#&#8203;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 ([#&#8203;416](home-operations/kopiur#416)) ([#&#8203;424](home-operations/kopiur#424)) ([ab1327a](home-operations/kopiur@ab1327a))
- **rust:** update crate rcgen (0.14.9 → 0.14.10) ([#&#8203;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) ([#&#8203;427](home-operations/kopiur#427)) ([4aee76d](home-operations/kopiur@4aee76d))
- **github-action:** update action anchore/sbom-action (v0.24.1 → v0.24.2) ([#&#8203;432](home-operations/kopiur#432)) ([0d029d4](home-operations/kopiur@0d029d4))
- **mise:** update tool aqua:astral-sh/uv (0.12.5 → 0.12.6) ([#&#8203;422](home-operations/kopiur#422)) ([d06d57c](home-operations/kopiur@d06d57c))
- **mise:** update tool aqua:astral-sh/uv (0.12.6 → 0.12.7) ([#&#8203;428](home-operations/kopiur#428)) ([36f8f56](home-operations/kopiur@36f8f56))
- **mise:** update tool aqua:dadav/helm-schema (0.23.4 → 0.23.5) ([#&#8203;412](home-operations/kopiur#412)) ([c1ad2a1](home-operations/kopiur@c1ad2a1))
- **mise:** update tool kubectl (1.36.4 → 1.37.0) ([#&#8203;426](home-operations/kopiur#426)) ([08eb492](home-operations/kopiur@08eb492))
- **mise:** update tool lefthook (2.1.11 → 2.1.12) ([#&#8203;430](home-operations/kopiur#430)) ([21312e5](home-operations/kopiur@21312e5))
- **mise:** update tool node (24.19.0 → v24.20.0) ([#&#8203;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 ([#&#8203;417](home-operations/kopiur#417)) ([ff8b396](home-operations/kopiur@ff8b396))
- manualRun explicit-null completedAt, tri-state Restore gate, throttle honored in every mover flow ([#&#8203;394](home-operations/kopiur#394), [#&#8203;393](home-operations/kopiur#393), [#&#8203;374](home-operations/kopiur#374)) ([#&#8203;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) ([#&#8203;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 ([#&#8203;397](home-operations/kopiur#397)) ([7c8dd3f](home-operations/kopiur@7c8dd3f))
- **mover:** read status.resolved via the /status subresource ([#&#8203;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) ([#&#8203;407](home-operations/kopiur#407)) ([b6d7d04](home-operations/kopiur@b6d7d04))
- **mise:** update mise tools ([#&#8203;400](home-operations/kopiur#400)) ([b1a83f8](home-operations/kopiur@b1a83f8))
- **mise:** update tool kubectl (1.36.3 → 1.36.4) ([#&#8203;405](home-operations/kopiur#405)) ([274e3d6](home-operations/kopiur@274e3d6))
- **mise:** update tool lefthook (2.1.10 → 2.1.11) ([#&#8203;406](home-operations/kopiur#406)) ([2ba700d](home-operations/kopiur@2ba700d))
- **mise:** update tool rust (1.97.1 → 1.98.0) ([#&#8203;403](home-operations/kopiur#403)) ([c655005](home-operations/kopiur@c655005))
- **mise:** update tool yq (4.53.3 → 4.53.4) ([#&#8203;402](home-operations/kopiur#402)) ([d1ed8a7](home-operations/kopiur@d1ed8a7))
- **mise:** update tool yq (4.53.4 → 4.53.6) ([#&#8203;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 ([#&#8203;380](home-operations/kopiur#380)) ([#&#8203;396](home-operations/kopiur#396)) ([74665dc](home-operations/kopiur@74665dc))

##### Bug Fixes

- **rust:** update crate cel (0.14.2 → 0.14.3) ([#&#8203;387](home-operations/kopiur#387)) ([bf9aa0c](home-operations/kopiur@bf9aa0c))
- **rust:** update crate http-body-util (0.1.4 → 0.1.5) ([#&#8203;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) ([#&#8203;388](home-operations/kopiur#388)) ([1040c9e](home-operations/kopiur@1040c9e))
- **mise:** update tool aqua:astral-sh/uv (0.12.3 → 0.12.4) ([#&#8203;390](home-operations/kopiur#390)) ([bf3916c](home-operations/kopiur@bf3916c))
- **mise:** update tool aqua:astral-sh/uv (0.12.4 → 0.12.5) ([#&#8203;395](home-operations/kopiur#395)) ([b919878](home-operations/kopiur@b919878))
- **mise:** update tool helm (4.2.3 → 4.2.4) ([#&#8203;389](home-operations/kopiur#389)) ([5b134fa](home-operations/kopiur@5b134fa))
- **mise:** Update tool oxfmt (0.62.0 → 0.63.0) ([#&#8203;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 ([#&#8203;382](home-operations/kopiur#382)) ([#&#8203;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 ([#&#8203;373](home-operations/kopiur#373)) ([fde8073](home-operations/kopiur@fde8073))
- **rust:** update crate cel (0.14.1 → 0.14.2) ([#&#8203;379](home-operations/kopiur#379)) ([4a60221](home-operations/kopiur@4a60221))
- **rust:** update crate futures (0.3.33 → 0.3.34) ([#&#8203;378](home-operations/kopiur#378)) ([ff657b8](home-operations/kopiur@ff657b8))
- **rust:** update crate rcgen (0.14.8 → 0.14.9) ([#&#8203;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) ([#&#8203;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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recovery from deleted bucket

1 participant