Skip to content

feat: multi-cluster shared repository support — cluster identity, foreign-aware discovery, collision-free maintenance leases - #251

Merged
perfectra1n merged 17 commits into
mainfrom
feat/multi-cluster-shared-repo
Jul 15, 2026
Merged

perfectra1n merged 17 commits into
mainfrom
feat/multi-cluster-shared-repo

Conversation

@perfectra1n

Copy link
Copy Markdown
Collaborator

Multi-cluster shared kopia repository support: a first-class cluster identity, own/foreign-aware discovery, collision-free maintenance leases — plus two standalone data-safety fixes this work uncovered.

Why

Sharing one kopia repository across clusters (the volsync-style "back up on cluster A, restore on cluster B" workflow) currently fights the operator on three fronts:

  1. Discovery assumes hostname == namespace: other clusters' snapshots warn DiscoveredSnapshotUnplaced or pile into fallbackNamespace.
  2. Identity collisions are a real data-loss vector: two clusters with same-named namespaces+policies write the same username@hostname:path; kopia dedups identical manifests, so one cluster's GFS prune can delete a manifest the other cluster's Snapshot CR still references.
  3. Maintenance leases collide: the managed lease was derived from the CR name only, so same-named ClusterRepositories on two clusters both "own" maintenance and run concurrent content GC.

What ships

One knob: identityDefaults.cluster (both repo kinds; RFC1123 label ≤32, dot-free). Setting it flips, coherently:

  • Default identity hostname becomes <namespace>.<cluster> (CEL gets a cluster variable; explicit overrides still win). Namespaces can't contain ., so the scheme is placement-invertible.
  • Discovery classifies own vs foreign (Bare / OwnCluster / ForeignCluster), with a typed catalog.foreignSnapshots: Ignore | Fallback policy. Foreign snapshots are counted (status.catalog.foreignSnapshotCount + kopiur_repo_foreign_snapshots gauge), never silently invisible — and never warned about when they're expected. The mover pre-filters foreign suffixes before its listing cap so a busy peer can't crowd your own snapshots out.
  • Maintenance leases become cluster-qualified and injective (kopiur/{cluster}/…, dot-joined kopia identity reserved to the kopiur prefix), with ownerAliases so the legacy owner upgrades automatically on the first claim, and RestampPolicy: OwnFormatsOnly so a cluster never clobbers a foreign owner. Exactly one cluster runs maintenance; others yield visibly.
  • mode: ReadOnly is now a safe consumer posture: bootstrap no longer stamps (or clobbers) the maintenance owner and connects with kopia's --readonly.
  • Repository identityDefaults edits are guarded at admission: identity re-resolves live on every run (the "pinned at admission" story in the docs was wrong), so an unguarded edit silently re-identified every consumer. Edits now require the allow-identity-change annotation when consumers have history, and list the affected policies as an admission warning.

Cross-cluster restore needed nothing new — Restore.spec.source.identity / fromPolicy already restore without a Snapshot CR; the new scenario page ties it together.

Standalone data-safety fixes (worth calling out — they affect every deployment, not just multi-cluster)

  • kopia's create-time retention was silently deleting CR-tracked snapshots. kopia snapshot create unconditionally applies the source's retention policy — and with none set, kopia's defaults (keep-latest 10 / hourly 48 / daily 7 / weekly 4 / monthly 24 / annual 3) delete manifests that Succeeded Snapshot CRs still reference, for any GFS window wider than those defaults. The mover now pins effectively-infinite kopia-side retention at the identity scope (CR-driven GFS is the sole deleter, as designed) and passes --no-auto-maintenance. Verified against the pinned kopia binary: 12 creates → 10 surviving manifests without the fix, 12 with it.
  • Snapshot matching was identity-blind. Delete self-heal, deep-verify resolution, and filesystem success resolution matched by source path only — /pvc/<name> collides across namespaces (and clusters). All matchers now filter by identity, with a same-path/different-hostname regression test at unit and e2e tiers.

Migration

docs/scenarios/shared-repository-multi-cluster.md documents the full live-migration story — the order is load-bearing (upgrade all clusters first; disable non-owner maintenance and remove takeoverPolicy: Force before any identity flip; the residual pre-migration dedup window and its deletionPolicy: Orphan mitigation). Setting cluster is unset-by-default and byte-for-byte back-compatible: every cluster-off code path is covered by fixture tests proving plans identical to before.

Verification

  • Full hermetic gate green at every one of the 17 commits (mise run test / clippy / complexity-check / gen-check), with each milestone independently re-verified.
  • New e2e suite (crates/e2e/tests/multi_cluster.rs): own-suffix placement, foreign ignore+count with zero unplaced warnings, legacy bare placement, lease yield + anti-restamp, legacy-owner alias upgrade, Fallback flip + restore-by-identity of foreign data, and the same-path/different-identity survival regression — plus the webhook acknowledgment flow. Green on fresh kind clusters (7/7 twice: the implementation run and an independent full-suite run).
  • Local full-suite runs show the four known machine-specific failures on the dev box (NFS pair, health-probe wipe detection, leak-sweep timing) — verified identical on a main-baseline worktree run, so pre-existing/environmental, not from this branch. CI is the arbiter for those suites.
  • kopia-binary integration test for the retention fix.

Related

  • Rejects the "archive Snapshot CRs" idea and splits the CR-count levers into Bound Snapshot CR reconcile cost at scale; document the math; no archive mode #249 (with rationale).
  • Follow-ups worth their own issues (from the final review): identity-guard degradation under Role-scoped installs (403 → same-namespace LIST fallback); small cleanups (matcher predicate unification, RestoreSource exhaustive-match refactor, restampPolicy serialization skip, milestone-marker comment sweep).

Several kopia-snapshot matchers resolved "which snapshot is mine" by source
path only, ignoring the recorded username/hostname. A path like /pvc/data
repeats across namespaces, and — in a shared repository — across clusters, so
a path-only match can silently select ANOTHER identity's snapshot: in the
delete self-heal (mover main.rs, after a recorded-id delete no-ops on a stale
id) this means deleting someone else's snapshot; in quick/deep verify and
restore-heal it means verifying or restoring the wrong source's data; in the
controller's in-process (filesystem-repo) success resolution it means
stamping the wrong snapshot id onto a Snapshot's status.

Fix: thread an optional (username, hostname) identity filter through every
matcher, applied in addition to path whenever the caller has one. Absent
identity (older anchors/work specs) falls back to today's path-only behavior
exactly — no CRD fields, no behavior change when identities are distinct per
source.

- SnapshotAnchor (mover work-spec wire type) gains optional username/hostname,
  serde-defaulted for back-compat; every producer (Snapshot delete/pin anchor,
  Restore source anchor) now populates them from the already-resolved
  identity in scope.
- match_current_manifest (mover resolve.rs) and resolve_latest_snapshot (mover
  main.rs, quick/deep verify + restore-heal) both require identity when given;
  share one pure predicate (matches_source).
- resolve_succeeded_snapshot's inline path-only match (controller snapshot/
  mod.rs) is extracted into a unit-tested pure predicate
  (matches_snapshot_identity) and now requires identity too. Fixes the stale
  "the source path IS authoritative" comment, which is false once two sources
  share a path.

Regression tests added at every layer: same path + different hostname ⇒ no
match (the core regression guard), same path + same identity ⇒ match, legacy
anchor/no-identity ⇒ path-only behavior preserved, ambiguity handling
unchanged, plus SnapshotAnchor serde back-compat/round-trip coverage.
…FS is the sole deleter

kopia's `snapshot create` unconditionally applies the source's own retention
policy after every create (policy.ApplyRetentionPolicy, even under
--override-source), and with no policy set kopia's own defaults apply
(keep-latest 10, hourly 48, daily 7, weekly 4, monthly 24, annual 3). Any
SnapshotPolicy.spec.retention window wider than those defaults had kopia
silently deleting manifests that Succeeded Snapshot CRs still reference,
surfacing only at restore. Kopiur's design is that CR-driven GFS is the SOLE
deleter; this restores that invariant via two mechanisms:

1. The mover now pins all six kopia --keep-* fields to KOPIA_KEEP_MAX
   (i32::MAX) at the identity scope before every snapshot create, regardless
   of whether a SnapshotPolicy configures anything (kopiur_kopia::PolicyArgs
   grew the six fields; not exposed on any CRD/workspec surface, matching
   the existing kopia-side-retention-forbidden rule).
2. Every KopiaClient now carries --no-auto-maintenance globally: kopia
   opportunistically runs maintenance as a side effect of snapshot
   create/delete/expire (and even a bare policy set) when the connected
   identity is the maintenance owner; only the Maintenance CR's explicit
   `maintenance run` may trigger it.

Verified against the pinned kopia 0.23.1 binary: twelve snapshot creates
under one identity with no override left only 10 manifests (kopia's own
keep-latest); with the identity-scope pin applied first, all 12 survived.
That is now a permanent integration regression guard.
…hostname classification

Multi-cluster shared-repository support (M1, api-crate only): N clusters
backing up same-named namespaces into one shared ClusterRepository currently
collide on kopia identity (hostname defaults to bare `<namespace>`), so one
cluster's GFS retention prune can delete another cluster's snapshots.

Adds `ClusterRepository.spec.identityDefaults.cluster` (an RFC 1123 label, no
dots — the first `.` in a hostname is now the namespace/cluster delimiter).
Hostname resolution precedence becomes: explicit `spec.identity.hostname`
override > `hostnameExpr` > `<namespace>.<cluster>` (cluster set) >
`namespace` (unchanged bare default). `cluster` is also always declared in
the identity CEL environment (`""` when unset), so `hostnameExpr`/
`usernameExpr` can reference it directly, e.g. `namespace + '.' + cluster`;
the admission-time trial context declares a non-empty placeholder so such an
expression validates regardless of whether this particular repository sets
`cluster`.

`classify_hostname`/`HostClass` give the read path (retention, discovered
Snapshot placement — later milestones) a total, pure way to tell whether a
stored hostname is this cluster's own, another cluster's, or predates cluster
identity (Bare) by splitting on the FIRST `.`. `validate_cluster_name` enforces
the RFC 1123 shape at admission (webhook) and defensively (controller), wired
into `validate_cluster_repository`.

Additive and back-compat: no schema default (context-dependent), absent field
behaves exactly as before. Placement, catalog, and webhook wiring beyond shape
validation are out of scope here and land in later milestones.

Includes regenerated deploy/crds, deploy/helm CRDs, and docs/field-reference.md
(cargo xtask gen-all).
…y consumers

Identity is re-resolved from the LIVE repository on every reconcile/backup
(controller/snapshot_policy.rs, io/repo.rs) — nothing about a
ClusterRepository's identityDefaults is pinned the way a policy's own
spec.identity is. So editing identityDefaults on a live ClusterRepository
silently re-identifies every consumer SnapshotPolicy on its next backup, with
no per-policy edit to acknowledge it: new snapshots fork to a new kopia
lineage while the old history orphans (GFS retention never prunes across the
split). The existing fork guard (identity_fork.rs) only fires on a
SnapshotPolicy's own UPDATE; nothing guarded the repository side of the same
hole. This also doubles as the migration gate for setting `cluster` on a
fleet with history (M1).

- kopiur-api: new pure `detect_repository_identity_change` (mirrors
  detect_identity_fork) plus `ValidationError::RepositoryIdentityWouldFork`,
  rendered via `describe_identity_change_consumers` (count + up to 5 names +
  "and N more") so the deny message and the ack warning name policies
  identically.
- kopiur-webhook: new identity_repo_edit module is the thin IO layer —
  cluster-wide SnapshotPolicy LIST (mirrors identity_collision.rs), keeping
  consumers that reference the edited repository, have snapshot history, and
  don't pin both username+hostname (fully pinned policies never consult
  defaults). Wired into handle_cluster_repository, which is now async and
  client-taking like the other handlers; the same
  kopiur.home-operations.com/allow-identity-change annotation from the
  per-policy guard acknowledges the change and surfaces an admission WARNING
  naming what was re-identified.
- Fail-open posture matches identity_fork/identity_collision: no client, or
  the consumer LIST failing (403 under a namespaced Role install, apiserver
  blip), degrades to allow with a tracing::warn! — a repository apply must
  never wedge on a best-effort admission check.
- Residual gap, by design: a delete + re-apply is a CREATE with no oldObject
  to diff, so it bypasses this guard exactly like the per-policy fork guard's
  CREATE gap.
- The namespaced Repository handler is made async/client-taking for
  signature parity, but RepositorySpec has no identityDefaults field yet
  (that's M5) — left a // M5: wiring note plus an #[ignore]d placeholder test
  that fails loudly if accidentally un-ignored.
- RBAC: no change. The webhook shares the operator ServiceAccount/ClusterRole,
  which already grants list/get/watch on snapshotpolicies cluster-wide;
  gen-check confirms no drift.
Adds the typed policy for what catalog discovery does with snapshots
classified as another cluster's (classify_hostname's ForeignCluster case),
plus status visibility. API crate + regenerated deploy/ only — the
controller consumes these in a later milestone.

- ForeignSnapshots enum (common/cache.rs): Ignore (default; skip, count
  only) or Fallback (materialize into catalog.fallbackNamespace,
  ClusterRepository only). Bare PascalCase wire strings, mirroring
  TakeoverPolicy. No schemars default: the effective default is coupled to
  identityDefaults.cluster, so it's context-dependent per the conventions
  doc, and must stay `—` in the field reference.
- CatalogBounds.foreignSnapshots + effective_foreign_snapshots() resolver,
  styled after effective_refresh_interval (absent catalog/field -> Ignore).
- CatalogStatus.foreignSnapshotCount: how many snapshots the last catalog
  scan classified as foreign, never materialized under Ignore.
- Validators, shared by the webhook and the controller via api::validate:
  (a) foreignSnapshots requires a cluster identity (identityDefaults.cluster)
      — foreign-vs-own-cluster classification is undecidable without one.
      Kind-neutral message: it fires unconditionally on a namespaced
      Repository (no identityDefaults field yet, M5) without claiming the
      field doesn't exist.
  (b) Fallback requires catalog.fallbackNamespace to be set.
  (c) Fallback is ClusterRepository-only, mirroring the existing
      fallbackNamespace-requires-cluster-scope rule.
  (d) A ClusterRepository with both identityDefaults.cluster and
      catalog.fallbackNamespace set must choose foreignSnapshots explicitly
      — adopting a cluster identity must never silently switch off an
      already-configured fallback collector (Ignore expires its rows under
      catalog.retain; Fallback keeps collecting). Two new ValidationError
      variants (ForeignSnapshotsRequiresCluster,
      ForeignSnapshotsChoiceRequired) carry (a)/(d); (b)/(c) reuse
      InvalidFieldValue inside validate_catalog_bounds, alongside the
      existing fallbackNamespace check it mirrors.

Regenerated deploy/crds, deploy/helm/kopiur/crds, docs/field-reference.md,
and the xtask CRD insta snapshot via `mise run gen`; `mise run gen-check`
is clean.
…n classification, foreign ignore/fallback, counting

M4 of multi-cluster shared-repo support: the catalog scan now consumes M1's
HostClass/classify_hostname and M3's catalog.foreignSnapshots instead of just
threading them through unused.

Architecture: one identity-aware placement pass runs over the FULL kopia
listing BEFORE plan_catalog, replacing the old per-entry-in-plan.create IO+
placement loop. Split like the rest of this module: decide_cluster_placement/
decide_namespace_placement + plan_placements are pure (unit-tested with a
stubbed ns_allowed map, no kube client); scan() does the thin IO (cached
Namespace GET + tenancy check per distinct candidate namespace) and feeds
plan_placements' foreign_ignored_ids into plan_catalog in the exact same
eligible-filter spot as produced_ids — so a ForeignIgnored entry never
consumes a retain.perIdentity slot, and any pre-existing discovered row for
one expires via the ordinary absence mechanics once it's no longer eligible.
Bare-hostname placement needs a namespace lookup only the controller can do,
which is why this pass has to run in the controller rather than being fully
front-loaded into the mover.

The one genuine ambiguity classify_hostname leaves the caller (a Bare
classification means EITHER no cluster identity at all, OR cluster identity
on but this hostname has no dot) is resolved with an explicit cluster_mode
bool parameter on decide_cluster_placement, keeping every row of the decision
table a literal, exhaustive match arm instead of duplicating placement logic
at the call site. Decision-table tests prove the cluster-off rows are
byte-identical to the deleted placement_namespace (migrated into catalog.rs).

Mover prefilter + cap interaction: BootstrapRepositoryOp gets
catalog_foreign_prefilter_cluster, armed by the controller only when cluster
mode is on AND the effective policy is Ignore (never under Fallback, which
needs the entries back). The mover's new apply_foreign_prefilter/
prepare_catalog_entries drop ForeignCluster-classified entries BEFORE
MAX_RETURNED_SNAPSHOTS is applied, so a busy foreign peer can no longer crowd
this cluster's own snapshots out of the capped listing; bare-hostname foreign
entries still count against the cap (they need the controller's namespace
lookup). BootstrapResult.foreign_suffix_dropped reports the count; the
controller adds it to its own foreign tally without double-counting, since a
mover-dropped entry never reaches the controller's listing at all.

Also: kopiur_repo_foreign_snapshots gauge + status.catalog.foreignSnapshotCount
(both repository kinds), a bounded top-5 foreign-suffix breakdown at info log
only (never status — cardinality is foreign-writer-controlled), and the
DiscoveredSnapshotUnplaced warning now also names the shared-repo fix
(identityDefaults.cluster + foreignSnapshots: Ignore).

No crates/api changes (gen-check confirms) — this task only makes the
controller/mover consume what M1/M3 already added.
…parity

Per-namespace Repository CRs pointing at one shared bucket have the same
cross-cluster identity-collision hazard as ClusterRepository (default hostname
== namespace). Gives RepositorySpec the same identityDefaults surface
ClusterRepository has had since M1, and activates every arm M1-M4 gated behind
`// M5:` stubs: the repo-edit identity-change guard's namespaced arm, the
catalog.foreignSnapshots x identityDefaults.cluster coupling (rule (a) now
accepts Ignore+cluster on a namespaced Repository; Fallback still rejected via
rule (c)), and the catalog placement pass's namespaced cluster-aware scan
(own/bare place, foreign ignored+counted).

IdentityDefaults moved from cluster_repository.rs into common/ (the crate's
existing home for types shared by both repository kinds), reusing rather than
duplicating the type; the public kopiur_api::IdentityDefaults path is
unchanged. The webhook's identity_collision/identity_fork/identity_repo_edit
guards now resolve a namespaced Repository's identityDefaults via a namespaced
GET (previously silently None), and handle_repository runs the same
fleet-wide identity-change guard handle_cluster_repository does, scoped to the
repository's own namespace (works under namespaced Role installs). The
Repository/ClusterRepository -> SnapshotPolicy referent watch needed for
prompt re-pinning already existed (controllers.rs) - verified, not modified.

mise run gen/gen-check clean; CRD schema shape matches ClusterRepository's
identityDefaults byte-for-byte (no schema default, same properties).
…(cross-namespace refs)

The `Repository` identityDefaults-edit guard scoped its consumer
`SnapshotPolicy` LIST to the Repository's own namespace, silently missing
consumers that reference it cross-namespace via a `RepositoryRef.namespace`
(a documented, supported pattern the controller already honors in
`repo_lookup`/`resolve_repository_ref`). A policy in another namespace could
never be found by that scoped LIST, so an identityDefaults edit could
silently re-identify its snapshot history with no deny and no warning.

Widen the consumer LIST to cluster-wide, exactly mirroring
`handle_cluster_repository`'s call and the pre-existing
`check_identity_collision` posture (`Api::all` unconditionally). Since both
call sites now always list cluster-wide, drop the now-dead `list_namespace`
scoping parameter from `check_repository_identity_change`/
`affected_consumers` rather than leave an unused footgun. The existing
`repo_key`-based consumer-match predicate already compares against the
ref's effective namespace (falling back to the owner's namespace only when
`ref.namespace` is unset), so no change was needed there.

Also correct doc comments that justified the old narrow scoping with an RBAC
argument that didn't hold, and update/add tests: rename the LIST-scope test
to assert cluster-wide for both repository kinds, and add a test proving a
cross-namespace consumer is caught and named in the deny message.
…alias migration + ReadOnly owner-stamp gating

Two clusters pointing a same-named ClusterRepository at one shared kopia
repository derive the SAME maintenance lease (managed_lease was name-only
for that kind), so both run concurrent full maintenance (content GC) on
the same data. kopia has no cross-host maintenance lock (its .mlock flock
is local-only) — the operator's owner lease is the only mutual exclusion,
so it must be collision-free. managed_lease now takes the repository's
identityDefaults.cluster and inserts it as a second path segment
(kopiur/{cluster}/... , exhaustive 4-arm match), giving each cluster a
distinct lease.

Naively per-cluster owners would PING-PONG: the bootstrap mover restamps
the stable owner on ANY mismatch on every connect-to-existing, so two
clusters would each see the other's owner as "stale" and re-claim it on
every bootstrap. Fixed by a restamp policy on the bootstrap work spec:
AnyStale (default, byte-compatible pre-existing behavior) vs
OwnFormatsOnly (used iff cluster is set), which only ever overwrites an
empty owner or one of its OWN recognized prior formats
(maintenance_owner_aliases) — never a foreign or unrecognized owner
(those need a one-time takeoverPolicy: Force; auto-clobber is exactly
what is unsafe on a shared repo). The same policy is applied by the
controller's in-process bare-path-filesystem restamp, which would
otherwise ping-pong against the repo's own managed Maintenance.

Migration rides Ownership.ownerAliases (new CRD field): the managed
Maintenance projected for a cluster-qualified repo carries its
pre-cluster lease as an alias, and the mover's lease decision
(lease_held_by_other) treats an alias-derived owner as SELF — the run
claims and re-stamps, upgrading the recorded owner with no manual Force.

A mode: ReadOnly consumer repository's bootstrap used to stamp/restamp
its own maintenance owner onto the shared repo, CLOBBERING the primary's
(a read-only consumer deciding who runs GC). Bootstrap work specs of
ReadOnly repos now carry maintenance_owner: None (as do repos with
maintenance disabled or covered by a foreign Maintenance) and connect
with --readonly (BootstrapRepositoryOp.read_only), making the probe
structurally unable to mutate the repository. Restore/delete movers are
unchanged (restore-heal legitimately writes pins).

The kopia owner identity derivation is now injective for
cluster-qualified leases: a 4-segment lease hostname is the segments
dot-joined, each sanitized individually with no per-segment cap, so
kopiur/east-prod/db/x and kopiur/east/prod-db/x no longer collide (the
old collapse-to-'-' + 63-char truncation made both collisions possible).
Legacy 3-segment and hand-authored leases keep byte-identical output.
…in derivation for kopiur-prefixed leases

Two Important fixes from the M6 (ea96631) review:

1. Regression: the in-process (bare-path filesystem) create-then-connect path
   hardcoded `created: false` into `maintenance_restamp_target` even right
   after its own successful create — unlike the mover, which stamps the
   maintenance owner unconditionally on create (crates/mover/src/main.rs).
   Consequence: a freshly-created Repository with `identityDefaults.cluster`
   set recorded the controller pod's ephemeral kopia identity as maintenance
   owner, and `RestampPolicy::OwnFormatsOnly` (required once `cluster` is set)
   then refused to ever restamp it (not empty, not a recognized alias) — the
   managed Maintenance's `takeoverPolicy: Never` yielded forever. Pre-M6
   healed this unconditionally on every reconcile, so this was a regression.

   Fixed by threading a real `created` flag through the in-process connect/
   create block in crates/controller/src/repository.rs, and adding
   `io::in_process_create_owner_target` (crates/controller/src/io/maintenance.rs)
   which stamps the desired owner unconditionally on `created`, mirroring the
   mover exactly, and otherwise defers to the existing self-heal path.

2. Hardening: `kopia_lease_identity`'s 4-segment dot-join arm matched ANY
   4-segment lease, including a hand-authored `Ownership.owner` like
   `a/b/c/d` that merely happens to split into 4 segments. That silently
   changed its derivation from the legacy `a-b-c-d` to `a.b.c.d` across the
   M6 upgrade, and with `takeoverPolicy: Never` maintenance would then yield
   forever. Gated the dot-join arm on the first segment being exactly
   `"kopiur"` (managed_lease's reserved prefix) so a hand-authored owner's
   derivation can never change across an upgrade merely by segment count.

Tests: a create-vs-connect owner-stamp matrix (crates/controller/src/io/tests.rs),
and a non-kopiur-prefixed 4-segment byte-identical-to-pre-M6 test plus updated
doc comment (crates/api/src/maintenance.rs). Full gate green: cargo test -p
kopiur-api -p kopiur-controller -p kopiur-mover, fmt-check, test, clippy,
complexity-check, gen-check.
…eign handling, lease migration, identity matching
…d repository guide + identity-resolution doc fixes

Ships the user-facing story for the multi-cluster shared-repository feature and
pays down the doc inaccuracies it uncovered:

- docs/repositories.md: document identityDefaults.cluster (RFC1123 <=32,
  dot-free, the <ns>.<cluster> default hostname, the CEL cluster variable, and
  the identity-change warning), and correct the ClusterRepository-exclusive
  framing of identityDefaults (Repository has had it since M5).
- docs/backups.md, crates/api/src/identity.rs, crates/controller/src/
  snapshot_policy.rs, crates/webhook/src/identity_repo_edit.rs, CLAUDE.md:
  fix the false "identity resolved once at admission, never re-rendered"
  claim - identity re-resolves from the live spec + live repository
  identityDefaults on every reconcile; per-policy edits are gated by the
  fork guard, repository identityDefaults edits by the repository-wide guard.
- docs/maintenance.md: the lease format table (legacy vs. cluster-qualified,
  including the dotted kopia hostname), ownerAliases, the one-owner-cluster
  posture, and RestampPolicy self-heal rules.
- docs/scenarios/migrate-across-clusters.md: distinguish a one-time app move
  from the new active-active shared-repo shape.
- NEW docs/scenarios/shared-repository-multi-cluster.md (+ nav + index row):
  why (3 collision surfaces), greenfield setup (both shapes), the ordered
  live-migration steps, edge notes, and a verification checklist.
- docs/troubleshooting.md: LeaseHeldByOther on a shared repo - expected vs.
  stale.
- deploy/examples: new scenario manifest (two ClusterRepository views of one
  bucket) + a commented cluster: line on 02-cluster-repository.yaml.
- Doc-debt: narrow the ForeignSnapshots foreign-counting wording in
  common/cache.rs (regenerated deploy/crds + docs/field-reference.md),
  mention `cluster` on IdentityInputs.defaults, mention the identity filter
  on the mover's resolve_pinned_info, and soften an overclaiming test comment
  in webhook/handlers.rs.

mise run fmt-check/test/clippy/complexity-check, gen/gen-check, and docs
(mkdocs --strict) all green.
kubectl kopiur status's REPOSITORIES table gains CLUSTER (spec.identityDefaults.
cluster) and FOREIGN (status.catalog.foreignSnapshotCount) columns, for both
Repository and ClusterRepository rows, so a shared repository's multi-cluster
state is visible on the same one-screen overview without a separate kubectl get.
… story — merged GFS timeline, not a frozen split

The identity-change warnings (docs/backups.md, docs/repositories.md,
IdentityWouldFork/RepositoryIdentityWouldFork in crates/api/src/error.rs, and
the identity_repo_edit module doc) claimed GFS "treats the old lineage as a
separate source and never prunes it." That's backwards: GFS pools ALL of a
policy's Snapshot CRs regardless of identity (backups_to_delete/select_kept
key on end_time/pinned only), so old- and new-lineage snapshots compete in one
merged timeline and the old lineage keeps aging out normally. The real
consequence is that restore/verify/fromPolicy resolve the new identity, so old
history is reachable only via Restore.source.identity. Reworded to match, and
updated the one test asserting the old error-message substring.
…aims

identity_fork.rs module doc and the allow-identity-change annotation doc in
consts.rs carried the same disproven framing the previous commit removed from
error.rs: GFS pools a policy's Snapshot CRs identity-blind into one merged
timeline; old-lineage CRs keep pruning (and their finalizers delete the old
kopia snapshots under deletionPolicy: Delete). The real consequences of an
identity change are restore/verify/fromPolicy rebinding to the new identity
and lineage interleaving in one GFS timeline — not an orphaned, never-pruned
old set. Also softened the residual 'orphan' lead-ins in backups.md.
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds multi-cluster support for shared Kopia repositories. The main changes are:

  • Cluster-aware repository identity through identityDefaults.cluster.
  • Own-vs-foreign catalog discovery with foreign snapshot policy and counts.
  • Cluster-qualified maintenance leases with alias-based migration support.
  • Safer read-only repository bootstrap behavior.
  • Identity-aware snapshot matching for restore, verify, and delete paths.
  • Admission guards for repository identity default changes.

Confidence Score: 5/5

This PR appears safe to merge.

No actionable bugs were identified in the reviewed identity, catalog, maintenance, mover, or webhook paths.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • The run captured tool-availability.log to surface missing external tools, including the command, working directory, and exit evidence.
  • Identity tests were executed and completed with exit code 0, as shown by the identity tests log.
  • Maintenance tests were executed and completed with exit code 0, as shown by the maintenance tests log.
  • The E2E compile harness validation ran in compile-only mode and finished with exit code 0.
  • Retention tests and related discovery were executed, including retention-test-list, retention-tests, and matcher-tests, all finishing with exit code 0.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
crates/api/src/identity.rs Adds cluster-aware identity resolution and hostname classification.
crates/api/src/validate/repository.rs Extends repository validation for cluster identity and foreign snapshot policy combinations.
crates/controller/src/catalog.rs Implements own/foreign catalog placement, filtering, and counting.
crates/controller/src/io/maintenance.rs Adds cluster-qualified maintenance owner planning, aliases, and read-only suppression.
crates/mover/src/main.rs Pins Kopia retention during snapshot creation and honors read-only bootstrap behavior.
crates/mover/src/bootstrap.rs Adds foreign-suffix prefiltering before the catalog materialization cap.
crates/mover/src/resolve.rs Adds identity-aware manifest matching to avoid same-path cross-identity collisions.
crates/webhook/src/identity_repo_edit.rs Adds the repository identityDefaults edit guard and acknowledgement flow.
crates/webhook/src/handlers.rs Wires identity default edit checks and warnings into repository admission handlers.
crates/controller/src/repository.rs Propagates cluster, foreign snapshot, read-only, and maintenance settings into bootstrap.
crates/controller/src/cluster_repository.rs Adds cluster repository bootstrap and catalog support for shared repositories.
crates/api/src/maintenance.rs Adds injective cluster-qualified maintenance lease identity derivation.

Reviews (1): Last reviewed commit: "docs(controller): correct PlacementPass:..." | Re-trigger Greptile

@perfectra1n
perfectra1n merged commit a4593d8 into main Jul 15, 2026
31 checks passed
@perfectra1n
perfectra1n deleted the feat/multi-cluster-shared-repo branch July 15, 2026 02:58
doonga pushed a commit to greyrock-labs/home-ops that referenced this pull request Jul 15, 2026
….7.3 ➔ 0.7.4) (#74)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | patch | `0.7.3` → `0.7.4` |

---

### Release Notes

<details>
<summary>home-operations/kopiur (ghcr.io/home-operations/charts/kopiur)</summary>

### [`v0.7.4`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#074-2026-07-15)

[Compare Source](home-operations/kopiur@0.7.3...0.7.4)

##### Features

- bound Snapshot CR reconcile cost at scale ([#&#8203;249](home-operations/kopiur#249)) ([#&#8203;253](home-operations/kopiur#253)) ([3697aa9](home-operations/kopiur@3697aa9))
- **docs:** clean up admonitions in docs ([71c5d85](home-operations/kopiur@71c5d85))
- multi-cluster shared repository support — cluster identity, foreign-aware discovery, collision-free maintenance leases ([#&#8203;251](home-operations/kopiur#251)) ([a4593d8](home-operations/kopiur@a4593d8))

##### Bug Fixes

- batch of six open-issue fixes ([#&#8203;248](home-operations/kopiur#248) [#&#8203;238](home-operations/kopiur#238) [#&#8203;245](home-operations/kopiur#245) [#&#8203;196](home-operations/kopiur#196) [#&#8203;237](home-operations/kopiur#237) [#&#8203;250](home-operations/kopiur#250)) ([#&#8203;252](home-operations/kopiur#252)) ([045a5a5](home-operations/kopiur@045a5a5))
- **deps:** update rust crate http-body-util (0.1.3 → 0.1.4) ([#&#8203;244](home-operations/kopiur#244)) ([4878006](home-operations/kopiur@4878006))
- **deps:** update rust crate rustls (0.23.41 → 0.23.42) ([#&#8203;243](home-operations/kopiur#243)) ([13b1dc4](home-operations/kopiur@13b1dc4))

##### Miscellaneous Chores

- **krew:** kopiur 0.7.3 manifest ([d521675](home-operations/kopiur@d521675))
- **mise:** Update tool aqua:EmbarkStudios/cargo-deny (0.19.9 → 0.20.2) ([#&#8203;226](home-operations/kopiur#226)) ([9c109b4](home-operations/kopiur@9c109b4))
- **mise:** Update tool oxfmt (0.58.0 → 0.59.0) ([#&#8203;246](home-operations/kopiur#246)) ([41625e9](home-operations/kopiur@41625e9))
- **mise:** Update tool zizmor (1.26.1 → 1.27.0) ([#&#8203;247](home-operations/kopiur#247)) ([fe10a6a](home-operations/kopiur@fe10a6a))

</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](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjAuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI2MC4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.greyrock.io/greyrock-labs/home-ops/pulls/74
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request Jul 16, 2026
….7.3 ➔ 0.7.4) (#1607)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | patch | `0.7.3` → `0.7.4` |

---

### Release Notes

<details>
<summary>home-operations/kopiur (ghcr.io/home-operations/charts/kopiur)</summary>

### [`v0.7.4`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#074-2026-07-15)

[Compare Source](home-operations/kopiur@0.7.3...0.7.4)

##### Features

- bound Snapshot CR reconcile cost at scale ([#&#8203;249](home-operations/kopiur#249)) ([#&#8203;253](home-operations/kopiur#253)) ([3697aa9](home-operations/kopiur@3697aa9))
- **docs:** clean up admonitions in docs ([71c5d85](home-operations/kopiur@71c5d85))
- multi-cluster shared repository support — cluster identity, foreign-aware discovery, collision-free maintenance leases ([#&#8203;251](home-operations/kopiur#251)) ([a4593d8](home-operations/kopiur@a4593d8))

##### Bug Fixes

- batch of six open-issue fixes ([#&#8203;248](home-operations/kopiur#248) [#&#8203;238](home-operations/kopiur#238) [#&#8203;245](home-operations/kopiur#245) [#&#8203;196](home-operations/kopiur#196) [#&#8203;237](home-operations/kopiur#237) [#&#8203;250](home-operations/kopiur#250)) ([#&#8203;252](home-operations/kopiur#252)) ([045a5a5](home-operations/kopiur@045a5a5))
- **deps:** update rust crate http-body-util (0.1.3 → 0.1.4) ([#&#8203;244](home-operations/kopiur#244)) ([4878006](home-operations/kopiur@4878006))
- **deps:** update rust crate rustls (0.23.41 → 0.23.42) ([#&#8203;243](home-operations/kopiur#243)) ([13b1dc4](home-operations/kopiur@13b1dc4))

##### Miscellaneous Chores

- **krew:** kopiur 0.7.3 manifest ([d521675](home-operations/kopiur@d521675))
- **mise:** Update tool aqua:EmbarkStudios/cargo-deny (0.19.9 → 0.20.2) ([#&#8203;226](home-operations/kopiur#226)) ([9c109b4](home-operations/kopiur@9c109b4))
- **mise:** Update tool oxfmt (0.58.0 → 0.59.0) ([#&#8203;246](home-operations/kopiur#246)) ([41625e9](home-operations/kopiur@41625e9))
- **mise:** Update tool zizmor (1.26.1 → 1.27.0) ([#&#8203;247](home-operations/kopiur#247)) ([fe10a6a](home-operations/kopiur@fe10a6a))

</details>

---

### Configuration

📅 **Schedule**: 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 [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/1607
hbjydev pushed a commit to hbjydev/phoebe that referenced this pull request Aug 27, 2026
…0.7.3 → 0.10.4) (#273)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/home-operations/charts/kopiur](https://github.com/home-operations/kopiur) | minor | `0.7.3` → `0.10.4` |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/141) for more information.

---

### Release Notes

<details>
<summary>home-operations/kopiur (ghcr.io/home-operations/charts/kopiur)</summary>

### [`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))

### [`v0.10.0`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#0100-2026-08-09)

[Compare Source](home-operations/kopiur@0.9.5...0.10.0)

##### ⚠ BREAKING CHANGES

- **rust:** Update crate base64 (0.22.1 → 0.23.1) ([#&#8203;366](home-operations/kopiur#366))

##### Features

- **rust:** Update crate base64 (0.22.1 → 0.23.1) ([#&#8203;366](home-operations/kopiur#366)) ([2c6e3e7](home-operations/kopiur@2c6e3e7))
- SnapshotReplication (logical snapshot-level replication) + SnapshotPolicy multi-repository fan-out ([#&#8203;370](home-operations/kopiur#370)) ([4e06e42](home-operations/kopiur@4e06e42))

##### Continuous Integration

- **github-action:** Update action Swatinem/rust-cache (v2.9.1 → v2.9.2) ([#&#8203;372](home-operations/kopiur#372)) ([e4ce173](home-operations/kopiur@e4ce173))

##### Miscellaneous Chores

- **mise:** Update mise tools ([#&#8203;367](home-operations/kopiur#367)) ([d6f82f4](home-operations/kopiur@d6f82f4))
- **mise:** Update tool cosign (3.1.2 → 3.1.3) ([#&#8203;371](home-operations/kopiur#371)) ([558b9f4](home-operations/kopiur@558b9f4))

### [`v0.9.5`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#095-2026-08-08)

[Compare Source](home-operations/kopiur@0.9.4...0.9.5)

##### Bug Fixes

- deliver s3 tls.caBundleRef to every kopia invocation ([#&#8203;365](home-operations/kopiur#365)) ([cda0237](home-operations/kopiur@cda0237))
- doctor misses structurally-blocked work ([#&#8203;359](home-operations/kopiur#359)) + phase/gate exhaustiveness ratchet ([#&#8203;363](home-operations/kopiur#363)) ([b844974](home-operations/kopiur@b844974))
- **rust:** update crate thiserror (2.0.19 → 2.0.20) ([#&#8203;360](home-operations/kopiur#360)) ([4aa646d](home-operations/kopiur@4aa646d))

##### Continuous Integration

- **github-action:** Update action docker/github-builder (v1.15.0 → v1.16.0) ([#&#8203;362](home-operations/kopiur#362)) ([db32818](home-operations/kopiur@db32818))

### [`v0.9.4`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#094-2026-08-08)

[Compare Source](home-operations/kopiur@0.9.3...0.9.4)

##### Features

- **docs:** Update README.md ([#&#8203;358](home-operations/kopiur#358)) ([05d2506](home-operations/kopiur@05d2506))

##### Bug Fixes

- implement pvcSelector/groupBy ([#&#8203;346](home-operations/kopiur#346)) and model deduped backups as Unchanged ([#&#8203;351](home-operations/kopiur#351)) ([#&#8203;354](home-operations/kopiur#354)) ([a2b9ae6](home-operations/kopiur@a2b9ae6))
- **rust:** update crate clap (4.6.5 → 4.6.6) ([#&#8203;357](home-operations/kopiur#357)) ([e9ca396](home-operations/kopiur@e9ca396))

##### Miscellaneous Chores

- **mise:** Update tool node (24.18.1 → v24.19.0) ([#&#8203;353](home-operations/kopiur#353)) ([2f28c6d](home-operations/kopiur@2f28c6d))
- **mise:** Update tool oxfmt (0.61.0 → 0.62.0) ([#&#8203;356](home-operations/kopiur#356)) ([e95df6b](home-operations/kopiur@e95df6b))

### [`v0.9.3`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#093-2026-08-05)

[Compare Source](home-operations/kopiur@0.9.2...0.9.3)

##### Features

- repository-level circuit breaker — stop fanning out doomed Jobs when the backend is unreachable ([#&#8203;350](home-operations/kopiur#350)) ([5d787b9](home-operations/kopiur@5d787b9))

##### Bug Fixes

- **rust:** update crate base64 (0.23.0 → 0.23.1) ([#&#8203;349](home-operations/kopiur#349)) ([11e7aa8](home-operations/kopiur@11e7aa8))

##### Continuous Integration

- **github-action:** Update action jdx/mise-action (v4.2.3 → v4.2.4) ([#&#8203;347](home-operations/kopiur#347)) ([1c9cf3b](home-operations/kopiur@1c9cf3b))

##### Miscellaneous Chores

- **renovate:** keep 0.x minors out of automerge ([#&#8203;352](home-operations/kopiur#352)) ([95d672a](home-operations/kopiur@95d672a))

### [`v0.9.2`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#092-2026-08-02)

[Compare Source](home-operations/kopiur@0.9.1...0.9.2)

##### Features

- **repository:** object-lock blob retention ([#&#8203;332](home-operations/kopiur#332)); fix blank volumeSnapshotClassName ([#&#8203;344](home-operations/kopiur#344)) ([dab8fdd](home-operations/kopiur@dab8fdd))

##### Bug Fixes

- **ci:** fail the merge gate on cancelled jobs, and cut e2e artifact time ([#&#8203;330](home-operations/kopiur#330)) ([1847a8a](home-operations/kopiur@1847a8a))
- **rust:** update crate time (0.3.54 → 0.3.55) ([#&#8203;337](home-operations/kopiur#337)) ([ee1b951](home-operations/kopiur@ee1b951))

##### Continuous Integration

- **github-action:** Update action docker/github-builder (v1.14.0 → v1.15.0) ([#&#8203;327](home-operations/kopiur#327)) ([f5514fc](home-operations/kopiur@f5514fc))
- **github-action:** Update action docker/login-action (v4.5.1 → v4.5.2) ([#&#8203;331](home-operations/kopiur#331)) ([d90efdf](home-operations/kopiur@d90efdf))
- **github-action:** Update action docker/login-action (v4.5.2 → v4.6.0) ([#&#8203;339](home-operations/kopiur#339)) ([b783bcc](home-operations/kopiur@b783bcc))
- **github-action:** Update action home-operations/.github/actions/workflow-lint (v1.0.2 → v1.0.3) ([#&#8203;343](home-operations/kopiur#343)) ([4af16d5](home-operations/kopiur@4af16d5))
- update shared actions and use self-repository syntax ([#&#8203;341](home-operations/kopiur#341)) ([559e23f](home-operations/kopiur@559e23f))

##### Miscellaneous Chores

- **deps:** lock file maintenance (pep621) ([#&#8203;334](home-operations/kopiur#334)) ([e19ae0e](home-operations/kopiur@e19ae0e))
- **mise:** Lock file maintenance tool (mise) ([#&#8203;335](home-operations/kopiur#335)) ([666afb5](home-operations/kopiur@666afb5))
- **mise:** prune lockfile to used platforms ([#&#8203;342](home-operations/kopiur#342)) ([741d6a4](home-operations/kopiur@741d6a4))
- **mise:** Update tool aqua:astral-sh/uv (0.12.0 → 0.12.1) ([#&#8203;333](home-operations/kopiur#333)) ([3ed2766](home-operations/kopiur@3ed2766))
- **mise:** Update tool promtool (3.13.1 → 3.13.2) ([#&#8203;329](home-operations/kopiur#329)) ([ac1752e](home-operations/kopiur@ac1752e))
- **mise:** Update tool zizmor (1.28.0 → 1.29.0) ([#&#8203;340](home-operations/kopiur#340)) ([410dc26](home-operations/kopiur@410dc26))
- **release-please:** standardize the release pull request title pattern ([#&#8203;338](home-operations/kopiur#338)) ([21013a3](home-operations/kopiur@21013a3))
- **rust:** lock file maintenance crate (cargo) ([#&#8203;336](home-operations/kopiur#336)) ([8606177](home-operations/kopiur@8606177))

### [`v0.9.1`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#091-2026-07-29)

[Compare Source](home-operations/kopiur@0.9.0...0.9.1)

##### Features

- **rust:** update crate http (1.4.2 → 1.5.0) ([#&#8203;323](home-operations/kopiur#323)) ([cf0b149](home-operations/kopiur@cf0b149))

##### Bug Fixes

- **controller:** survive a slow API server instead of abdicating on the first one ([#&#8203;319](home-operations/kopiur#319)) ([#&#8203;324](home-operations/kopiur#324)) ([df7e90c](home-operations/kopiur@df7e90c))
- **rust:** update crate rustls (0.23.42 → 0.23.43) ([#&#8203;326](home-operations/kopiur#326)) ([2f5123e](home-operations/kopiur@2f5123e))

##### Tests

- **e2e:** retry flaky tests with cargo-nextest ([#&#8203;322](home-operations/kopiur#322)) ([558ac85](home-operations/kopiur@558ac85))

##### Miscellaneous Chores

- **mise:** Update tool aqua:astral-sh/uv (0.11.33 → 0.12.0) ([#&#8203;320](home-operations/kopiur#320)) ([66ec350](home-operations/kopiur@66ec350))
- **mise:** Update tool node (24.18.0 → v24.18.1) ([#&#8203;325](home-operations/kopiur#325)) ([1c833a4](home-operations/kopiur@1c833a4))

### [`v0.9.0`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#090-2026-07-28)

[Compare Source](home-operations/kopiur@0.8.1...0.9.0)

##### ⚠ BREAKING CHANGES

- **rust:** Update crate serial\_test (3.5.0 → 4.0.1) ([#&#8203;307](home-operations/kopiur#307))
- **secctx:** identity-aware mover merge, recorded snapshot identity, restore inherit-from-snapshot ([#&#8203;304](home-operations/kopiur#304))

##### Features

- **rust:** Update crate serial\_test (3.5.0 → 4.0.1) ([#&#8203;307](home-operations/kopiur#307)) ([4392e4a](home-operations/kopiur@4392e4a))
- **secctx:** identity-aware mover merge, recorded snapshot identity, restore inherit-from-snapshot ([#&#8203;304](home-operations/kopiur#304)) ([0632a29](home-operations/kopiur@0632a29))

##### Bug Fixes

- **rust:** update crate cel (0.14.0 → 0.14.1) ([#&#8203;317](home-operations/kopiur#317)) ([00cf539](home-operations/kopiur@00cf539))
- **rust:** update crate schemars (1.2.1 → 1.2.2) ([#&#8203;313](home-operations/kopiur#313)) ([b6e6268](home-operations/kopiur@b6e6268))

##### Build System

- **mise:** add actionlint and refresh the lockfile ([#&#8203;309](home-operations/kopiur#309)) ([e74c9b3](home-operations/kopiur@e74c9b3))

##### Continuous Integration

- adopt the shared workflow-lint and docs-build actions ([#&#8203;310](home-operations/kopiur#310)) ([7d4b329](home-operations/kopiur@7d4b329))
- gate pull requests on Build Success and share the docs build ([#&#8203;306](home-operations/kopiur#306)) ([1b693aa](home-operations/kopiur@1b693aa))
- **github-action:** Update action docker/login-action (v4.5.0 → v4.5.1) ([#&#8203;312](home-operations/kopiur#312)) ([29d00b1](home-operations/kopiur@29d00b1))
- **github-action:** Update action jdx/mise-action (v4.2.1 → v4.2.2) ([#&#8203;311](home-operations/kopiur#311)) ([65bbca4](home-operations/kopiur@65bbca4))
- **github-action:** Update action jdx/mise-action (v4.2.2 → v4.2.3) ([#&#8203;315](home-operations/kopiur#315)) ([5f517d4](home-operations/kopiur@5f517d4))
- skip release-please PRs in checks and drop nightly e2e ([#&#8203;305](home-operations/kopiur#305)) ([ee6d4f5](home-operations/kopiur@ee6d4f5))

##### Miscellaneous Chores

- **krew:** kopiur 0.8.1 manifest ([84df757](home-operations/kopiur@84df757))
- **mise:** Update tool aqua:astral-sh/uv (0.11.32 → 0.11.33) ([#&#8203;318](home-operations/kopiur#318)) ([f63bc3a](home-operations/kopiur@f63bc3a))
- **mise:** Update tool oxfmt (0.60.0 → 0.61.0) ([#&#8203;314](home-operations/kopiur#314)) ([bdbf6c6](home-operations/kopiur@bdbf6c6))
- **renovate:** drop the stale automerge overrides ([#&#8203;308](home-operations/kopiur#308)) ([c13a3d5](home-operations/kopiur@c13a3d5))
- standardize release-please changelog sections ([#&#8203;316](home-operations/kopiur#316)) ([273d0f8](home-operations/kopiur@273d0f8))

### [`v0.8.1`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#081-2026-07-24)

[Compare Source](home-operations/kopiur@0.8.0...0.8.1)

##### Features

- **deps:** update rust crate base64 (0.22.1 → 0.23.0) ([#&#8203;293](home-operations/kopiur#293)) ([5ab14d5](home-operations/kopiur@5ab14d5))
- **deps:** update rust crate kube (4.0.0 → 4.2.0) ([#&#8203;288](home-operations/kopiur#288)) ([b067458](home-operations/kopiur@b067458))
- httpRequest hook headers ([#&#8203;290](home-operations/kopiur#290)) + recovery-aware alerts ([#&#8203;280](home-operations/kopiur#280)) ([#&#8203;296](home-operations/kopiur#296)) ([f388a14](home-operations/kopiur@f388a14))
- **renovate:** automerge if it passes CI ([66f243f](home-operations/kopiur@66f243f))

##### Bug Fixes

- **controller:** survive an API-server outage without exhausting file descriptors ([#&#8203;287](home-operations/kopiur#287)) ([ce184b6](home-operations/kopiur@ce184b6))
- **deps:** update rust crate clap (4.6.2 → 4.6.3) ([#&#8203;277](home-operations/kopiur#277)) ([e3d4113](home-operations/kopiur@e3d4113))
- **deps:** update rust crate libc (0.2.186 → 0.2.189) ([#&#8203;283](home-operations/kopiur#283)) ([2b2e636](home-operations/kopiur@2b2e636))
- **deps:** update rust crate serde\_json (1.0.150 → 1.0.151) ([#&#8203;275](home-operations/kopiur#275)) ([1410f42](home-operations/kopiur@1410f42))
- **deps:** update rust crate time (0.3.53 → 0.3.54) ([#&#8203;276](home-operations/kopiur#276)) ([ff250ff](home-operations/kopiur@ff250ff))
- **deps:** update rust crate tokio (1.53.0 → 1.53.1) ([#&#8203;281](home-operations/kopiur#281)) ([82082e9](home-operations/kopiur@82082e9))
- **helm:** stamp Chart.yaml version on release + document image sub-fields ([#&#8203;289](home-operations/kopiur#289)) ([041549a](home-operations/kopiur@041549a))
- **probe:** stamp the health probe at launch so it stops recycling its Job ([#&#8203;273](home-operations/kopiur#273)) ([#&#8203;278](home-operations/kopiur#278)) ([7fa3657](home-operations/kopiur@7fa3657))
- **release:** use the generic updater for Chart.yaml + stamp README badges ([#&#8203;292](home-operations/kopiur#292)) ([f4a3141](home-operations/kopiur@f4a3141))
- retention-aware adoption stops the adopt/prune/rediscover livelock ([#&#8203;299](home-operations/kopiur#299)) ([4093f12](home-operations/kopiur@4093f12))

##### Styles

- indent markdown at 2 to match embedded yaml ([#&#8203;279](home-operations/kopiur#279)) ([06885e0](home-operations/kopiur@06885e0))

##### Miscellaneous Chores

- **github-release:** Update release helm-unittest/helm-unittest (v1.1.1 → v1.1.2) ([#&#8203;301](home-operations/kopiur#301)) ([e62272d](home-operations/kopiur@e62272d))
- **krew:** kopiur 0.8.0 manifest ([2be416f](home-operations/kopiur@2be416f))
- **mise:** Update tool aqua:astral-sh/uv (0.11.29 → 0.11.30) ([#&#8203;282](home-operations/kopiur#282)) ([e12f96c](home-operations/kopiur@e12f96c))
- **mise:** Update tool aqua:astral-sh/uv (0.11.30 → 0.11.31) ([#&#8203;286](home-operations/kopiur#286)) ([a3f31e6](home-operations/kopiur@a3f31e6))
- **mise:** Update tool aqua:astral-sh/uv (0.11.31 → 0.11.32) ([#&#8203;298](home-operations/kopiur#298)) ([9d8c171](home-operations/kopiur@9d8c171))
- **mise:** Update tool kubectl (1.36.2 → 1.36.3) ([#&#8203;291](home-operations/kopiur#291)) ([ef290f8](home-operations/kopiur@ef290f8))
- **mise:** Update tool oxfmt (0.59.0 → 0.60.0) ([#&#8203;284](home-operations/kopiur#284)) ([ad8a18a](home-operations/kopiur@ad8a18a))
- **mise:** Update tool rust (1.96.1 → 1.97.1) ([#&#8203;222](home-operations/kopiur#222)) ([fbeb2fa](home-operations/kopiur@fbeb2fa))
- **mise:** Update tool zizmor (1.27.0 → 1.28.0) ([#&#8203;285](home-operations/kopiur#285)) ([9ef35ce](home-operations/kopiur@9ef35ce))

### [`v0.8.0`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#080-2026-07-19)

[Compare Source](home-operations/kopiur@0.7.5...0.8.0)

##### ⚠ BREAKING CHANGES

- SnapshotPolicy deletion cascade + auto-adoption of discovered snapshots ([#&#8203;272](home-operations/kopiur#272))

##### Features

- **deps:** update rust crate tokio (1.52.4 → 1.53.0) ([#&#8203;264](home-operations/kopiur#264)) ([6cab0c6](home-operations/kopiur@6cab0c6))
- mass-deletion protection (cascade guard, per-repo breaker, batched deletes) ([#&#8203;265](home-operations/kopiur#265)) ([038ea6d](home-operations/kopiur@038ea6d))
- SnapshotPolicy deletion cascade + auto-adoption of discovered snapshots ([#&#8203;272](home-operations/kopiur#272)) ([9da8c26](home-operations/kopiur@9da8c26))

##### Bug Fixes

- **deps:** update rust crate anyhow (1.0.103 → 1.0.104) ([#&#8203;269](home-operations/kopiur#269)) ([4d81496](home-operations/kopiur@4d81496))
- **deps:** update rust crate clap (4.6.1 → 4.6.2) ([#&#8203;257](home-operations/kopiur#257)) ([c238df1](home-operations/kopiur@c238df1))
- **deps:** update rust crate futures (0.3.32 → 0.3.33) ([#&#8203;268](home-operations/kopiur#268)) ([926b52d](home-operations/kopiur@926b52d))
- **deps:** update rust crate serde (1.0.228 → 1.0.229) ([#&#8203;270](home-operations/kopiur#270)) ([5fbd713](home-operations/kopiur@5fbd713))
- **deps:** update rust crate thiserror (2.0.18 → 2.0.19) ([#&#8203;271](home-operations/kopiur#271)) ([d83730f](home-operations/kopiur@d83730f))
- **deps:** update rust crate tokio (1.52.3 → 1.52.4) ([#&#8203;261](home-operations/kopiur#261)) ([accf6ae](home-operations/kopiur@accf6ae))
- writable backup source ([#&#8203;254](home-operations/kopiur#254)), stuck deletion finalizer ([#&#8203;255](home-operations/kopiur#255)), kopia epoch parameters ([#&#8203;258](home-operations/kopiur#258)) ([#&#8203;263](home-operations/kopiur#263)) ([94f9a15](home-operations/kopiur@94f9a15))

##### Miscellaneous Chores

- **krew:** kopiur 0.7.5 manifest ([16d7390](home-operations/kopiur@16d7390))
- **mise:** Update tool aqua:astral-sh/uv (0.11.28 → 0.11.29) ([#&#8203;256](home-operations/kopiur#256)) ([f822335](home-operations/kopiur@f822335))
- **mise:** Update tool cosign (3.1.1 → 3.1.2) ([#&#8203;267](home-operations/kopiur#267)) ([649ba2e](home-operations/kopiur@649ba2e))

### [`v0.7.5`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#075-2026-07-16)

[Compare Source](home-operations/kopiur@0.7.4...0.7.5)

##### Bug Fixes

- **readme:** update readme admonition ([987957c](home-operations/kopiur@987957c))
- **secctx:** SecurityContextCompatible is verified, not asserted; inherit merges with explicit ([#&#8203;259](home-operations/kopiur#259)) ([cbfd97d](home-operations/kopiur@cbfd97d))

### [`v0.7.4`](https://github.com/home-operations/kopiur/blob/HEAD/CHANGELOG.md#074-2026-07-15)

[Compare Source](home-operations/kopiur@0.7.3...0.7.4)

##### Features

- bound Snapshot CR reconcile cost at scale ([#&#8203;249](home-operations/kopiur#249)) ([#&#8203;253](home-operations/kopiur#253)) ([3697aa9](home-operations/kopiur@3697aa9))
- **docs:** clean up admonitions in docs ([71c5d85](home-operations/kopiur@71c5d85))
- multi-cluster shared repository support — cluster identity, foreign-aware discovery, collision-free maintenance leases ([#&#8203;251](home-operations/kopiur#251)) ([a4593d8](home-operations/kopiur@a4593d8))

##### Bug Fixes

- batch of six open-issue fixes ([#&#8203;248](home-operations/kopiur#248) [#&#8203;238](home-operations/kopiur#238) [#&#8203;245](home-operations/kopiur#245) [#&#8203;196](home-operations/kopiur#196) [#&#8203;237](home-operations/kopiur#237) [#&#8203;250](home-operations/kopiur#250)) ([#&#8203;252](home-operations/kopiur#252)) ([045a5a5](home-operations/kopiur@045a5a5))
- **deps:** update rust crate http-body-util (0.1.3 → 0.1.4) ([#&#8203;244](home-operations/kopiur#244)) ([4878006](home-operations/kopiur@4878006))
- **deps:** update rust crate rustls (0.23.41 → 0.23.42) ([#&#8203;243](home-operations/kopiur#243)) ([13b1dc4](home-operations/kopiur@13b1dc4))

##### Miscellaneous Chores

- **krew:** kopiur 0.7.3 manifest ([d521675](home-operations/kopiur@d521675))
- **mise:** Update tool aqua:EmbarkStudios/cargo-deny (0.19.9 → 0.20.2) ([#&#8203;226](home-operations/kopiur#226)) ([9c109b4](home-operations/kopiur@9c109b4))
- **mise:** Update tool oxfmt (0.58.0 → 0.59.0) ([#&#8203;246](home-operations/kopiur#246)) ([41625e9](home-operations/kopiur@41625e9))
- **mise:** Update tool zizmor (1.26.1 → 1.27.0) ([#&#8203;247](home-operations/kopiur#247)) ([fe10a6a](home-operations/kopiur@fe10a6a))

</details>

---

### Configuration

📅 **Schedule**: (in timezone Europe/London)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, 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:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjAuMiIsInVwZGF0ZWRJblZlciI6IjQ0LjQ2LjUiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbInJlbm92YXRlL2NvbnRhaW5lciIsInR5cGUvbWlub3IiXX0=-->

Reviewed-on: https://forgejo.hayden.moe/hayden/phoebe/pulls/273
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.

1 participant