Migrating from VolSync¶
Translate VolSync ReplicationSource/ReplicationDestination objects into
kopiur SnapshotPolicy/SnapshotSchedule/Restore manifests (printed as
apply-ready YAML; --apply creates them via server-side apply). All
global flags apply.
Two VolSync movers are supported, with very different data semantics —
the command auto-detects which one each object uses (spec.restic vs
spec.kopia), and a mixed namespace translates in one run:
| Mover | Where it comes from | What migration means |
|---|---|---|
| restic | upstream VolSync | Config translation ONLY — the repository formats are incompatible, so the kopiur repository starts empty. |
| kopia | the perfectra1n/volsync fork | The repository is a kopia repository: kopiur adopts it in place — all existing snapshots are preserved and history continues. |
| Flag | Effect |
|---|---|
--name NAME |
Translate one ReplicationSource (default: every one in the namespace). |
--repository NAME [--repository-kind …] |
Point the translated policies at an EXISTING kopiur repository. |
--resolve-secrets |
Instead, parse each repository Secret and EMIT a kopiur Repository derived from it. restic: + credential Secrets, with a REPLACE_ME kopia password you must set (a kopia repo needs its own new password); --apply refuses while any placeholder remains. kopia (fork): the existing repository is adopted — the Secret is referenced in place, no placeholder, so --apply works in one shot. |
--include-destinations |
Also translate ReplicationDestinations into Restores. restic (and kopia without an identity): deploy-or-restore fromPolicy + onMissingSnapshot: Continue. kopia with sourceIdentity or username/hostname: a raw-identity restore (source.identity), no policy pairing needed. |
--strict |
Exit 1 (emitting nothing) when any field has no kopiur equivalent. A minimal fork-kopia source is fully mappable and passes. |
--apply |
Server-side-apply the translated objects. |
-f, --filename PATH |
Read VolSync objects from a YAML file, a directory, or - (stdin) instead of the cluster — no kubeconfig required. Repeatable. See Offline / GitOps mode. |
--secrets PATH |
In offline mode, resolve repository Secrets from plaintext Secret YAML on disk (file/dir/-). Repeatable; needs --resolve-secrets. |
--from-cluster-secrets |
In offline mode, fetch the referenced Secrets from the live cluster instead of --secrets. |
--out-dir DIR |
Write one YAML file per ReplicationSource (plus _shared.yaml for derived Repositories/Secrets) into DIR instead of stdout. |
--force |
Allow --out-dir to overwrite existing files. |
Every VolSync field the translator reads is accounted for on stderr as
mapped (with the kopiur destination), UNMAPPABLE (with why and what to do
instead — e.g. restic's retain.within has no kopia equivalent), or
ignored (with why it isn't needed — e.g. pruneIntervalDays: kopiur
maintenance is default-managed). Nothing is silently dropped.
Offline / GitOps mode¶
If your VolSync objects live as YAML in a Git repository (not just in the
cluster), point migrate volsync at the files with -f/--filename — a file,
a directory (every *.yaml/*.yml in it), or - for stdin. With -f, the
command reads nothing from the cluster unless you ask it to, so it needs no
kubeconfig:
$ kubectl kopiur migrate volsync -f ./apps/media/volsync.yaml --repository nas --out-dir ./apps/media/kopiur
wrote 2 file(s) to ./apps/media/kopiur:
_shared.yaml
media-app.yaml
--out-dir writes one file per ReplicationSource (named after it), plus a
_shared.yaml holding any derived Repository/credential Secrets shared across
sources — ready to commit. Without --out-dir, the manifests stream to stdout as
before. You can also pipe the cluster's objects through it:
kubectl get replicationsource -A -o yaml | kubectl kopiur migrate volsync -f - --repository nas (a kind: List is unwrapped automatically).
Credentials offline have three options (your SOPS-encrypted Secrets can't be read straight from Git):
| You want | Use |
|---|---|
Point policies at a kopiur Repository you author/migrate separately — no Secret reads |
--repository NAME |
Derive the Repository from plaintext Secret YAML on disk |
--resolve-secrets --secrets ./secrets.yaml |
| Read VolSync from files but fetch Secrets from the live cluster (e.g. Flux already decrypted them) | --resolve-secrets --from-cluster-secrets |
--apply is rejected with offline input — review the emitted files and
kubectl apply -f them yourself. Each object's namespace comes from its own
metadata.namespace; -n only supplies a fallback for namespace-less objects
and never filters offline input.
Try it offline¶
deploy/examples/tryit/volsync-offline.yaml is a GitOps-shaped bundle: two fork-kopia ReplicationSources in the apps namespace plus the repository Secrets they reference. Nothing here touches a cluster — you can run the whole arc on your laptop with no kubeconfig.
# Two fork-kopia ReplicationSources — one file per app comes out, named after
# each source (blog.yaml, forum.yaml). `repository` names the Secret below.
apiVersion: volsync.backube/v1alpha1
kind: ReplicationSource
metadata:
name: blog
namespace: apps
spec:
sourcePVC: blog-data
trigger:
schedule: "0 2 * * *"
kopia:
repository: blog-kopia-repo # -> Secret blog-kopia-repo (below)
copyMethod: Direct
retain:
daily: 7
weekly: 4
---
apiVersion: volsync.backube/v1alpha1
kind: ReplicationSource
metadata:
name: forum
namespace: apps
spec:
sourcePVC: forum-data
trigger:
schedule: "30 3 * * *"
kopia:
repository: forum-kopia-repo # -> Secret forum-kopia-repo (below)
copyMethod: Direct
retain:
daily: 14
monthly: 6
1. Point the policies at an existing kopiur Repository — the simplest path, no Secret reads at all:
$ kubectl kopiur migrate volsync -f deploy/examples/tryit/volsync-offline.yaml --repository nas --out-dir ./migrated
wrote 2 file(s) to ./migrated:
blog.yaml
forum.yaml
You get one file per source — migrated/blog.yaml and migrated/forum.yaml — each a SnapshotPolicy + SnapshotSchedule pointing at the nas repository, ready to commit.
2. Or derive the Repository from the Secrets with --resolve-secrets. The Secrets live in the same bundle: -f reads only the ReplicationSources and --secrets reads only the Secrets, so one file feeds both flags (in a real repo, decrypt your SOPS Secrets to a scratch file first):
# The repository Secrets, as plaintext for offline `--resolve-secrets --secrets`.
# In a real GitOps repo these are SOPS-encrypted; decrypt them to a scratch file
# for the migrate run, or skip them entirely with `--repository`/
# `--from-cluster-secrets`. KOPIA_REPOSITORY picks the backend to ADOPT;
# KOPIA_PASSWORD + AWS_* are referenced in place by the emitted Repository.
apiVersion: v1
kind: Secret
metadata:
name: blog-kopia-repo
namespace: apps
type: Opaque
stringData:
KOPIA_REPOSITORY: "s3://my-kopia-bucket/blog/"
KOPIA_PASSWORD: "REPLACE_ME" # the EXISTING repo password — reuse it
AWS_ACCESS_KEY_ID: "REPLACE_ME"
AWS_SECRET_ACCESS_KEY: "REPLACE_ME"
AWS_S3_ENDPOINT: "s3.us-east-1.amazonaws.com"
---
apiVersion: v1
kind: Secret
metadata:
name: forum-kopia-repo
namespace: apps
type: Opaque
stringData:
KOPIA_REPOSITORY: "s3://my-kopia-bucket/forum/"
KOPIA_PASSWORD: "REPLACE_ME" # the EXISTING repo password — reuse it
AWS_ACCESS_KEY_ID: "REPLACE_ME"
AWS_SECRET_ACCESS_KEY: "REPLACE_ME"
AWS_S3_ENDPOINT: "s3.us-east-1.amazonaws.com"
$ kubectl kopiur migrate volsync -f deploy/examples/tryit/volsync-offline.yaml \
--resolve-secrets --secrets deploy/examples/tryit/volsync-offline.yaml --out-dir ./migrated --force
wrote 3 file(s) to ./migrated:
_shared.yaml
blog.yaml
forum.yaml
Now migrated/_shared.yaml carries the two derived Repository objects (each adopting the fork repo in place — no create block, the Secret referenced where it sits), and the per-source files reference them. Fork-kopia has no password placeholder, so the result is apply-ready.
Already running against the cluster?
Drop --secrets for --from-cluster-secrets and the same command reads the live Secrets instead — handy when Flux has already decrypted them into the cluster. Or pipe the cluster's objects straight in: kubectl get replicationsource -A -o yaml | kubectl kopiur migrate volsync -f - --repository nas.
3. Review and apply. --apply is refused offline by design; commit the files, or apply them yourself:
restic sources (upstream VolSync)¶
Config translation ONLY — no data is migrated
A VolSync restic repository is NOT a kopia repository. The kopiur repository the translated policies point at starts empty and fills as kopiur takes its own snapshots. Keep VolSync (and its repository) running until kopiur's retention coverage is sufficient for your recovery needs.
With --resolve-secrets, the restic Secret's RESTIC_REPOSITORY URL is
parsed into a kopiur Repository backend (s3/b2/azure/gcs/filesystem) and the
backend credentials are carried into a new <secret>-kopiur-creds Secret
under kopia's env names (restic's differ for B2/Azure). The kopia
password is emitted as a REPLACE_ME placeholder you must replace — a restic
password cannot initialize a kopia repository's encryption.
kopia sources (perfectra1n/volsync fork)¶
Repository ADOPTED in place — data and history are preserved
The fork's mover writes a real kopia repository. The emitted Repository
connects to it as-is (same backend, same password) with no create
block — it must already exist, so a mis-parsed backend can never silently
initialize a fresh empty repository. Every existing snapshot is preserved and
surfaces as an origin: discovered Snapshot.
What the translation does, and why it is safe to switch over:
- Identity continuity (the load-bearing part). The fork records snapshots
as
<sanitized-name>@<sanitized-namespace>:/data(or your explicitusername/hostname/sourcePathOverride). Every translatedSnapshotPolicypinsspec.identity+sources[0].sourcePathOverrideto exactly that identity, so the next kopiur snapshot continues the same history — retention sees old and new snapshots as one series. The pinned identity is shown in the accounting ((fork snapshot identity)line); verify it matcheskopia snapshot listbefore applying. - Secrets are referenced in place, never copied. The
Repository'sencryption.passwordSecretRefpoints at the existing VolSync Secret'sKOPIA_PASSWORD, and (for S3, whoseAWS_*env names already match) the backendauth.secretRefdoes too. Only key names kopia does not read get a small derived<secret>-kopiur-credsrename-Secret: B2 (B2_ACCOUNT_ID/B2_APPLICATION_KEY→B2_KEY_ID/B2_KEY), legacy Azure (AZURE_ACCOUNT_KEY→AZURE_STORAGE_KEY), WebDAV (WEBDAV_USERNAME/WEBDAV_PASSWORD→KOPIA_WEBDAV_*), and SFTP known-hosts data. KOPIA_REPOSITORYURL formss3://,gcs://,azure://,b2://,filesystem://(the repository PVC is inferred frommoverVolumes),sftp://,webdav://, andrclone://all translate, including the fork's Secret-key overrides (KOPIA_S3_BUCKETbeats the URL bucket,AWS_S3_ENDPOINT,*_DISABLE_TLS, …). Parity quirks are preserved: onlys3://bucket/prefixcarries a prefix (with the fork's trailing slash); forgcs/azure/b2the fork always ignored the URL's path portion, so the repository is adopted at the bucket/container root and the dropped path is called out in the accounting.gdrive://has no kopiur backend — author thatRepositoryby hand.- Retention maps 1:1 (
retain.latest→keepLatest,retain.yearly→keepAnnual, the rest by name), as docompression,parallelism(→upload.maxParallelFileReads),additionalArgs(→extraArgs), and the cache size limits (→mover.cache.*).
KEEP the VolSync Secret(s)
kopiur reads the repository password (and, where the names match, the backend
credentials) from the original VolSync Secret, in place. When you
decommission VolSync, delete its CRDs and ReplicationSources — but keep the
repository Secret, or the adopted Repository loses its credentials.
Retire the fork's KopiaMaintenance objects
kopiur manages repository maintenance itself and takes over kopia's
maintenance ownership (kopia maintenance set --owner) on its first run. A
fork KopiaMaintenance left running will fight kopiur over ownership —
delete it once the adopted repository is Ready.
A few fork fields have no kopiur equivalent and are called out in the
accounting instead of being silently dropped: actions.beforeSnapshot/
afterSnapshot run in the fork's mover pod, while kopiur
hooks run in the workload pod — rewrite them for
that context; policyConfig raw policy files are replaced by the typed
SnapshotPolicy fields; shallow restore windows have no analog (use
asOf/offset/snapshotID).
Suggested cut-over¶
- Suspend or delete the fork's
ReplicationSource(so the two operators don't snapshot concurrently), keep its Secret. kubectl kopiur migrate volsync -n <ns> --resolve-secrets— review the accounting, especially the pinned identity line.- Re-run with
--apply; wait for theRepositoryto reachReadyand the old snapshots to appear:kubectl kopiur snapshots list -n <ns>. - Prove continuity:
kubectl kopiur snapshot now --policy <name> --wait, then confirm the new snapshot lists under the same identity. - Delete the fork's
KopiaMaintenanceobjects and, when satisfied, the VolSync install — not the repository Secret.
Try it end-to-end¶
Translate a fork-kopia VolSync source into kopiur manifests, review the accounting, apply it, and confirm the repository is adopted.
Prerequisite: a fork-kopia source
This arc reads one apply-ready input, deploy/examples/tryit/volsync-source.yaml: a fork-kopia ReplicationSource (spec.kopia) in media plus the repository Secret it references. It assumes the upstream VolSync CRDs are installed and kubectl kopiur is on your PATH (see the playground setup for the install).
# A minimal fork-kopia ReplicationSource. A minimal source is fully mappable and
# passes `--strict`. `repository` names the Secret above; `spec.kopia` marks it
# as the fork mover (vs upstream `spec.restic`).
apiVersion: volsync.backube/v1alpha1
kind: ReplicationSource
metadata:
name: media
namespace: media
spec:
sourcePVC: app-data
trigger:
schedule: "0 2 * * *"
kopia:
repository: media-kopia-repo
copyMethod: Direct
retain:
daily: 7
weekly: 4
Fill in the AWS_* keys and the existing repo KOPIA_PASSWORD (the fork already initialized this repo — reuse its password), then apply the input:
1. Dry-run the translation — nothing is changed; the accounting prints on stderr:
$ kubectl kopiur migrate volsync -n media --resolve-secrets
kopia sources: REPOSITORY ADOPTED IN PLACE — all existing snapshots are preserved and the snapshot identity is pinned so history continues. KEEP the referenced VolSync Secret(s); retire the fork's KopiaMaintenance objects.
mapped spec.sourcePVC -> SnapshotPolicy.spec.sources[0].pvc.name
mapped spec.kopia.retain -> SnapshotPolicy.spec.retention
mapped spec.trigger.schedule -> SnapshotSchedule.spec.schedule.cron
...
# (the kopiur Repository + SnapshotPolicy + SnapshotSchedule YAML on stdout)
Review the accounting — especially the pinned (fork snapshot identity) line — before applying. Every field the translator reads is mapped, UNMAPPABLE, or ignored; nothing is silently dropped.
2. Apply when the accounting looks right — fork-kopia adopts the repository in place, so --apply works in one shot (no REPLACE_ME to fill):
$ kubectl kopiur migrate volsync -n media --resolve-secrets --apply
...
applied Repository/media
applied SnapshotPolicy/media
applied SnapshotSchedule/media
3. Confirm the repository is healthy (deep) with doctor:
4. See the adopted history. The fork's existing snapshots surface as origin: discovered:
$ kubectl kopiur snapshots list --origin discovered -n media
NAME POLICY ORIGIN PHASE SNAPSHOT-ID SIZE FILES START AGE
media-20260601-020000 - discovered Succeeded 9f8e7d6c5b4a 4.7 GiB 982 2026-06-01T02:00:00Z 10d
Illustrative — discovered rows need a real fork repository
The --origin discovered listing above is illustrative: it appears only once the catalog scan finds real snapshots in the adopted repository. Against an empty placeholder backend the table is empty until the fork's repo (with history) is reachable. The verbatim parts are the adoption banner, the mapped/applied <kind>/<name> line formats, and the ok repositories ready doctor line.