Scenario 05 — Adopt an existing kopia repository
You already have a kopia repository. You created it by hand, or a cron job running the kopia CLI did, or another tool did. Now you want Kopiur to take it over: see the old snapshots, restore from them, run maintenance, and back up going forward. All of that without re-uploading data or stranding the existing snapshots.
What adoption does, step by step
Section titled “What adoption does, step by step”flowchart TB REPO[Repository<br/>connect: create.enabled=false] --> DISC[Kopiur discovers foreign snapshots<br/>origin=discovered, Retain] REPO --> MNT[Maintenance<br/>take over the lease] DISC --> RST[Restore one discovered snapshot<br/>prove it works] REPO --> BC[SnapshotPolicy<br/>identity matched to the foreign writer] BC --> SCH[SnapshotSchedule]
The four deliberate moves in the bundle:
- Connect, don’t create.
create.enabled: falseadopts the existing repository and never re-initializes it. TheKOPIA_PASSWORDis the existing one. - Take over maintenance. A standalone
Maintenancewith an explicitownershiplease, so Kopiur and the old tooling don’t both runkopia maintenance. We disable theRepository’s default-managed maintenance withspec.maintenance.enabled: false, so the takeover is deliberate rather than automatic. - Prove it. Restore one discovered snapshot into a throwaway PVC.
- Back up going forward, matching identity. Pin the new
SnapshotPolicy’sidentityto the foreign writer’susername@hostname:path, so new snapshots dedup against the existing timeline and extend it.
# Scenario 05 — Adopt an existing kopia repository## You already have a kopia repository — created by hand, by k8up, or by a cron# job running the kopia CLI — and you want Kopiur to take it over: see the old# snapshots, restore from them, run maintenance, and back up going forward# WITHOUT re-uploading data or stranding the old snapshots.## The bundle:# 1. Repository — CONNECTS to the existing repo (create.enabled: false).# 2. (automatic) — Kopiur DISCOVERS the foreign snapshots and materializes them# as Snapshot CRs with origin=discovered, forced# deletionPolicy: Retain (it never deletes data it didn't# create). List them:# kubectl get snapshots -n adopt \# -l kopiur.home-operations.com/origin=discovered# 3. Maintenance — TAKE OVER the maintenance lease from the old tooling, so two# writers don't both run `kopia maintenance`.# 4. Restore — restore one discovered snapshot (proves adoption worked).# 5. SnapshotPolicy+Schedule — back up going forward. Pin `identity` to MATCH the# foreign writer's username@hostname:path so new snapshots# dedup against, and extend, the existing timeline.## Field shapes verified against crates/api. takeover_policy is a closed enum:# Never (default) | PromptCondition | Force.---apiVersion: v1kind: Secretmetadata: name: legacy-repo-creds namespace: adopttype: OpaquestringData: AWS_ACCESS_KEY_ID: "REPLACE_ME" AWS_SECRET_ACCESS_KEY: "REPLACE_ME" # The EXISTING repo's password — exactly as the old tooling set it. KOPIA_PASSWORD: "REPLACE_ME_with_the_EXISTING_repo_password"---apiVersion: kopiur.home-operations.com/v1alpha1kind: Repositorymetadata: name: legacy-primary namespace: adoptspec: backend: s3: bucket: my-existing-kopia-bucket prefix: "" # match wherever the old tooling put the repo (often the root) endpoint: s3.us-east-1.amazonaws.com region: us-east-1 auth: secretRef: name: legacy-repo-creds encryption: passwordSecretRef: name: legacy-repo-creds key: KOPIA_PASSWORD create: enabled: false # adopt: the repo already exists, never re-initialize it # Discovery knobs (all optional). An initial scan always runs on connect; here we # opt in to PERIODIC re-scans so snapshots the OLD tooling keeps writing during the # migration window keep appearing (off by default — each re-scan re-runs the # bootstrap Job). retain bounds the discovered Snapshot CR ROWS for very large # histories (expiring a row never deletes the kopia snapshot behind it). catalog: periodicRefresh: true # opt in to repeated re-scans (default false = scan once) refreshInterval: 1h # cadence when periodicRefresh is on (default 1h, minimum 30s) retain: perIdentity: 100 # newest N rows per username@hostname:path maxAgeDays: 90 # no rows for snapshots older than this # Don't let the default-managed Maintenance auto-claim the lease here — we want # the deliberate, explicit takeover below instead. (An externally-authored # Maintenance like the one in this file is always honored and never duplicated.) maintenance: enabled: false---# Deliberately take over maintenance from the previous owner.apiVersion: kopiur.home-operations.com/v1alpha1kind: Maintenancemetadata: name: legacy-primary-maintenance namespace: adoptspec: repository: kind: Repository name: legacy-primary schedule: quick: cron: "0 */6 * * *" jitter: 30m full: cron: "0 3 * * 0" jitter: 1h timezone: UTC ownership: owner: "kopiur/legacy-primary" # PromptCondition surfaces the conflict on status/conditions and waits for you # to confirm; switch to Force to seize the lease immediately once you've # stopped the old maintenance job. Never (the default) would refuse to take a # lease another writer holds. takeoverPolicy: PromptCondition---# Prove adoption: restore one discovered snapshot into a throwaway PVC. Replace# the name with one from the `origin=discovered` list above.apiVersion: kopiur.home-operations.com/v1alpha1kind: Restoremetadata: name: adopt-smoke-test namespace: adoptspec: source: snapshotRef: name: kopia-disc-9c2a1f # a discovered Snapshot CR (see the kubectl get above) target: pvc: name: adopt-smoke-test storageClassName: fast-ssd capacity: 10Gi accessModes: - ReadWriteOnce policy: onMissingSnapshot: Fail---# Back up going forward, CONTINUING the foreign writer's identity so kopia dedups# against the existing data instead of re-uploading a fresh copy.apiVersion: kopiur.home-operations.com/v1alpha1kind: SnapshotPolicymetadata: name: app-data namespace: adoptspec: repository: name: legacy-primary sources: - pvc: name: app-data # Match EXACTLY what the old tooling recorded (inspect a discovered Snapshot's # status, or `kopia snapshot list`, to read the existing username@hostname:path). identity: username: app-data hostname: legacy-host retention: keepDaily: 14 keepWeekly: 6---apiVersion: kopiur.home-operations.com/v1alpha1kind: SnapshotSchedulemetadata: name: app-data-nightly namespace: adoptspec: policyRef: name: app-data schedule: cron: "H 2 * * *" jitter: 30m runOnCreate: falseMatching the foreign identity
Section titled “Matching the foreign identity”This is the field most likely to trip you up. New backups only dedup against the old data if Kopiur writes under the same identity the previous tool used.
Inspect a discovered Snapshot’s status, or run kopia snapshot list against the repository, to read the existing username@hostname:path. Then set:
identity: username: app-data # the existing snapshot's user hostname: legacy-host # the existing snapshot's hostIf you don’t match it, backups still succeed. But they start a brand-new lineage and re-upload a full copy instead of an incremental one.
Verify adoption
Section titled “Verify adoption”$ kubectl get repository legacy-primary -n adoptNAME PHASE AGElegacy-primary Ready 25s
$ kubectl get maintenance legacy-primary-maintenance -n adoptNAME REPOSITORY OWNED AGElegacy-primary-maintenance legacy-primary true 25s
$ kubectl get restore adopt-smoke-test -n adoptNAME PHASE AGEadopt-smoke-test Completed 45sA Ready repository, an OWNED maintenance lease, and a Completed smoke-test restore mean the repository is fully adopted.
Delete a policy, then recreate it
Section titled “Delete a policy, then recreate it”Adoption isn’t only for a repository you’re onboarding for the first time. It is also how Kopiur heals itself after you delete a SnapshotPolicy and bring it back. Here is that scenario end to end, continuing from the postgres-data recipe above once it’s been backing up for a while:
flowchart LR DEL[Delete SnapshotSchedule + SnapshotPolicy] --> CLEAN[Snapshot CRs removed<br/>kopia data kept, Retain] CLEAN --> REAPPLY[kubectl apply the SAME SnapshotPolicy] REAPPLY --> SCAN[No history yet -> requests<br/>an on-demand catalog scan] SCAN --> DISC[Repository re-scans -><br/>origin=discovered rows] DISC --> ADOPT[Exact identity match -><br/>origin=adopted, config-labeled] ADOPT --> RET[GFS retention resumes]
-
Delete the schedule, then the policy. A GitOps prune can remove both at once; either order ends in the same outcome. See Backups → Retain-wins-ties. With the default
onPolicyDelete: Retain, thepolicy-cleanupfinalizer removes everySnapshotCR carrying the policy’s config label, but every kopia snapshot stays in the repository. Nothing is deleted on the kopia side.Terminal window $ kubectl delete snapshotschedule postgres-data-nightly -n billing$ kubectl delete snapshotpolicy postgres-data -n billing -
Re-apply the same
SnapshotPolicy, with the same name and the sameidentityand sources, as shown below. A freshly-created policy has noSnapshotCRs carrying its config label yet, so it has no history. Its first reconcile finds nothing to adopt, because the old snapshots haven’t been rediscovered yet. Since it has no history and hasn’t already asked for this exact identity, it requests an on-demand catalog scan on the repository, instead of waiting for a spec change or the periodic-refresh timer that is off by default. AnAdoptionScanRequestedNormal Event fires on theSnapshotPolicy, naming the identity it’s waiting on. -
The repository honors the scan request and re-lists the backend. That re-creates the kept kopia snapshots as
origin: discoveredrows, exactly like the very first adoption above, just triggered on demand instead of by the initial bootstrap. -
The policy adopts them on its next reconcile. It matches discovered rows by exact structured identity:
usernameANDhostnameANDsourcePathmust ALL match its own resolved identity. There is never a partial or fuzzy match. It creates anorigin: adoptedSnapshotCR carrying the config label for each one, and removes the matching discovered rows. ASnapshotsAdoptedNormal Event fires naming the count and identity. -
Retention resumes. Adopted rows are governed by GFS retention exactly like produced ones, so
spec.retentionstarts pruning them the moment they age out of the window. What happens to history outside a narrower window depends on the policy’s effectivedefaultDeletionPolicy:Delete, the default: everything matching is adopted, and the out-of-window rows are pruned on the very next reconcile, kopia data included. That is intended, not a bug. See Troubleshooting if this surprises you.RetainorOrphan: pruning would delete only the CR while the kopia snapshot survives, to be re-discovered and re-adopted forever. So Kopiur adopts only the in-window candidates and deliberately leaves the rest asdiscoveredrows.status.adoption.skippedByRetentioncounts them, and anAdoptionSkippedByRetentionNormal Event on the policy names the levers.
# Example 36 — Recreating a deleted SnapshotPolicy: catalog.adoption + the# automatic delete -> recreate -> adopt -> retention-resumes flow## Pairs with example 35 (SnapshotPolicy.spec.deletion.onPolicyDelete: Retain,# the default): after that cascade removes a policy's Snapshot CRs but keeps# every kopia snapshot in the repository, re-applying THIS SAME manifest is# how the history comes back and retention resumes pruning it. Step by step:## 1. `kubectl delete snapshotschedule postgres-data-nightly` then# `kubectl delete snapshotpolicy postgres-data` (or a GitOps prune removing# both at once — see docs/src/content/docs/backups.mdx "Retain-wins-ties"). The policy's# Snapshot CRs are cleaned up; every kopia snapshot survives, un-cataloged.# 2. `kubectl apply -f` this file again (a genuinely fresh SnapshotPolicy has# no Snapshot CRs carrying its config label — no history yet). Its first# reconcile finds zero adoption candidates for its resolved identity# (nothing has been rediscovered yet) and, because it has no history and# hasn't already asked for this identity, requests an on-demand catalog# scan on the repository (an AdoptionScanRequested Normal Event fires on# the SnapshotPolicy) instead of waiting for a spec change or the# (off-by-default) periodicRefresh timer.# 3. The repository reconciler honors that request and re-scans, which# re-materializes the kept kopia snapshots as `origin: discovered` rows# (status.catalog.scanRequestHonored advances to match the request).# 4. The SnapshotPolicy's next reconcile sees those discovered rows, matches# them by EXACT identity (username + hostname + sourcePath — never a# fuzzy/partial match), and adopts them: a SnapshotsAdopted Normal Event# fires, each becomes an `origin: adopted` Snapshot CR carrying this# policy's config label, and the matching `origin: discovered` rows are# removed (recreate-then-delete, never a re-label, so a crash mid-wave# just re-runs next pass).# 5. Adopted rows are now GFS-governed exactly like produced ones — the SAME# spec.retention below prunes them once they age out of the window. This# is by design, not a bug: see docs/src/content/docs/backups.mdx "Retention" and# "troubleshooting.md" if backups you didn't expect to lose disappear# right after a recreate. (That destructive prune applies under the# default `defaultDeletionPolicy: Delete`. Under Retain/Orphan, out-of-# window history is never adopted at all — it stays `discovered`, counted# in status.adoption.skippedByRetention — because a CR-only prune would# just re-discover and re-adopt forever.)## On a repository SHARED by many namespaces/policies, this scan-request is# bounded: many policies recreated at once each stamp the SAME repository# annotation, but the repository only ever needs to run ONE scan to satisfy# all of them (a fresh scan re-materializes everything at once) — it is not# one scan per policy.## Field shapes verified against crates/api (externally-tagged backend:# `backend.s3`, not `backend: { kind: S3 }`).## See docs/src/content/docs/repositories.mdx ("The catalog — discovered snapshots"),# docs/src/content/docs/backups.mdx ("What happens when the policy is deleted"), and# docs/src/content/docs/scenarios/adopt-existing-repo.mdx ("Delete a policy, then recreate it").---apiVersion: v1kind: Secretmetadata: name: nas-primary-creds namespace: billingtype: OpaquestringData: AWS_ACCESS_KEY_ID: "REPLACE_ME" AWS_SECRET_ACCESS_KEY: "REPLACE_ME" KOPIA_PASSWORD: "choose-something-long-and-random"---apiVersion: kopiur.home-operations.com/v1alpha1kind: Repositorymetadata: name: nas-primary namespace: billingspec: backend: s3: bucket: my-backups prefix: prod/ endpoint: s3.us-east-1.amazonaws.com region: us-east-1 auth: secretRef: name: nas-primary-creds encryption: passwordSecretRef: name: nas-primary-creds key: KOPIA_PASSWORD create: enabled: true catalog: # Whether a discovered snapshot whose identity matches a live # SnapshotPolicy is automatically re-attached to it. Adopt is the # effective default even with this whole `catalog` block omitted; shown # explicitly here since it's the field this example is about. A # per-policy `spec.adoption` (see the SnapshotPolicy below) overrides # this per-repository setting. adoption: Adopt # Adopt (default) | Ignore---apiVersion: kopiur.home-operations.com/v1alpha1kind: SnapshotPolicymetadata: name: postgres-data namespace: billingspec: repository: kind: Repository name: nas-primary sources: - pvc: name: postgres-data # GFS retention — applies identically to produced AND adopted Snapshots. # Recreating this policy and letting adoption run is only useful if this # window is wide enough to actually keep the history you're recovering. retention: keepDaily: 14 keepWeekly: 4 # Per-policy override of automatic adoption for discovered snapshots whose # resolved identity matches THIS recipe. Absent (the normal case) inherits # the repository's `catalog.adoption` above. Set explicitly here only if # this one recipe should behave differently from its repository's default # — e.g. a policy you deliberately want to start a fresh lineage under # rather than re-absorb a foreign/prior history: # adoption: IgnoreSee also
Section titled “See also”- Restores → discovered snapshots and example 07: the two ways to restore foreign snapshots.
- Maintenance and example 08: ownership leases and takeover policy in full.
- Backups → identity: matching the foreign writer’s identity.
- Backups → What happens when the policy is deleted: the
onPolicyDeletecascade this scenario relies on. - Repositories → The catalog: the
catalog.adoptionknob and scan-request mechanics in full.