Skip to content

Restores

A Restore reads a snapshot back into a PVC. At its core it answers three questions: from where (source), to where (target), and how (options/policy). For real-world restores it also exposes the same mover knobs a backup has — UID/GID, kopia cache, and a Job retry/deadline policy — covered in Mover, cache & failure policy below.

The shape of a Restore

spec:
    source: { <one of three>: ... } # FROM: which snapshot
    target: { <one of three>: ... } # TO: pvc | pvcRef | populator: {}  (REQUIRED)
    options: { ... } # HOW kopia writes (file deletion, permissions)
    policy: { ... } # what to do if the snapshot is missing

source and target are required; options/policy are optional with safe defaults.

Restore is "pick a row, write it somewhere" — there's no timestamp arithmetic in the common case. A Restore resolves its source once at admission and pins it to status, so it never silently retargets a different snapshot later.

Where to restore fromsource

Exactly one of three modes (externally tagged — you set one key):

snapshotRef — restore a specific Snapshot (the default)

You browsed the catalog and picked a Snapshot CR. No timestamps — just reference it. See example 03.

source:
    snapshotRef:
        name: postgres-data-20260524-021300
        namespace: billing # optional; defaults to the Restore's namespace

To find candidates:

$ kubectl get snapshots -n billing \
    -l kopiur.home-operations.com/config=postgres-data \
    --sort-by=.status.timing.startTime

fromPolicy — resolve via a SnapshotPolicy's identity

Restore the latest (or an offset/point-in-time) snapshot for a SnapshotPolicy's identity — even when no Snapshot CR exists yet. This is what powers deploy-or-restore (see below) and point-in-time rollback (example 14, scenario 07). Defaults to onMissingSnapshot: Continue.

source:
    fromPolicy:
        name: postgres-data
        namespace: billing # optional; defaults to the Restore's namespace
        offset: 0 # 0 = latest, 1 = previous, ...
        # asOf: 2026-05-01T00:00:00Z   # or: newest snapshot at/before this instant

asOf (newest snapshot at/before an RFC3339 instant) and offset (count back from latest) usually travel alone — asOf is the "roll back to a known-good time" knob; offset is "the previous one." They do compose if you set both: asOf filters first, then offset counts back within what's left ("the one before the last known-good").

The resolution is pinned — a restore never silently retargets

Whatever the source resolves to is written ONCE to status.resolved.kopiaSnapshotID and reused for the rest of the restore's life. New snapshots appearing mid-flight (a schedule firing) cannot change which snapshot this Restore writes.

fromPolicy / identity-without-snapshotID resolve "latest" on every backend

Resolving "latest / asOf / offset for a policy" lists the repository's snapshots inside the restore mover Job, which reaches every backend the same way a backup does — so this works on S3, Azure, GCS, B2, SFTP, WebDAV, rclone, and filesystem alike. No controller-side repo mount is needed.

If no matching snapshot exists yet, onMissingSnapshot applies (Continue comes up empty — deploy-or-restore; Fail fails the Restore). waitTimeout keeps the Job re-checking for the snapshot to appear before that decision — so it must be shorter than the Job's failurePolicy.activeDeadlineSeconds (the admission webhook enforces this when you set both).

identity — a raw kopia identity

For snapshots written by a foreign kopia client, or ones that have aged out of the catalog (example 13). You give the raw username@hostname:path. This mode requires an explicit spec.repository (there's no Snapshot/SnapshotPolicy to infer it from).

repository: { kind: Repository, name: primary, namespace: backups }
source:
    identity:
        username: postgres-data
        hostname: billing
        sourcePath: /data
        snapshotID: k1f1ec0a8 # pin an exact snapshot, or use asOf / offset

Where to restore totarget

pvc — create a new PVC

The operator creates the PVC and restores into it. Best for verification restores (restore alongside the original and compare):

target:
    pvc:
        name: postgres-data-restored
        storageClassName: fast-ssd # optional; cluster default otherwise
        capacity: 100Gi # REQUIRED: the operator won't guess a size it creates
        accessModes: [ReadWriteOnce] # optional; defaults to [ReadWriteOnce]

capacity is required (webhook-enforced): the operator creates this PVC, and a guessed default could be smaller than the data being restored. Size it at least as large as the source. The created PVC is deliberately not owned by the Restore — deleting the Restore CR afterwards leaves the restored data in place.

pvcRef — write into an existing PVC

target:
    pvcRef:
        name: postgres-data # an existing PVC in this namespace

The restore mover must mount this PVC to write into it. For a ReadWriteOnce target held by a running pod, Kopiur co-locates the mover onto that node automatically; a ReadWriteOncePod target can't be co-mounted at all while a pod holds it — scale the workload down first (see PVC access modes & RWOP).

populator: {} — passive populator mode

Set target.populator: {} and the Restore becomes a passive volume-populator source: it doesn't act on its own; instead a PVC's spec.dataSourceRef points at it, and the snapshot is restored as that PVC is provisioned. This is the GitOps deploy-or-restore pattern (next section).

target:
    populator: {} # explicit passive-populator mode

target is required — the empty-target form is gone

A Restore with no target is rejected by the webhook. Populator intent must be the explicit target.populator: {} (not an omitted target). The live-pod inheritSecurityContextFrom modes (workloadSelector/pvcConsumer) are invalid in populator mode — there's no workload pod at provision time, so the webhook rejects them and points you at moverDefaults / an explicit securityContext / inheritSecurityContextFrom: { snapshot: {} }, which is allowed: it replays the identity recorded on the backup and needs no live pod (see the re-bootstrap section).

A populator Restore is reusable — recreate the PVC and it restores again

A populator Restore is a living source, not a one-shot. Completed reports the last populate; it does not latch the Restore "consumed". Every PVC that claims it via dataSourceRef is populated as that PVC is provisioned — so if you delete the claiming PVC and apply a new one with the same dataSourceRef, Kopiur restores into the new PVC again. You don't touch the Restore at all: the PVC event re-enqueues it, and a populator's Completed phase is deliberately not terminal until a bound consumer exists, so a fresh, unbound claim drives a new populate.

The catch is which snapshot: it re-restores the one pinned in status.resolved at the first resolution, not whatever is newest — the same pin-once rule that governs every source on this page. To pick up a newer snapshot, delete and re-create the Restore so it re-resolves.

Contrast a direct target (pvc / pvcRef): that restore is one-shot. Once Completed it's terminal, and deleting then re-creating the target PVC does not re-restore — create a new Restore to restore again.

Re-creating a populator Restore over a bound PVC does nothing (by design)

The claim is what drives a populate, not the Restore. A volume populator can only hand a volume to an unbound claim, so if you delete and re-create the Restore (a GitOps prune and re-apply, a repository rebuild that cascades) while its claiming PVC is still Bound and the app is happily running on it, there is nothing to populate. Kopiur completes it as a no-op: Completed with Ready=True reason=TargetAlreadyBound, no prime PVC, no mover run, and your live volume untouched.

To actually restore into that claim, delete the PVC (keeping its dataSourceRef) and let it be re-created — see the reusability note above.

Upgrading from ≤ 0.7.x? That case used to run a full restore into a prime-<uid> PVC that could never be adopted, and then leak it: a Bound PVC holding a complete second copy of your data, one per re-created Restore. On upgrade, Kopiur reaps those automatically (watch for an OrphanedPrimePvcReaped event) for every orphan whose claiming PVC still exists. Any left over from a claim that has since been deleted are garbage-collected when their Restore is. To find them: kubectl get pvc -A -l kopiur.home-operations.com/op=restore-populate.

How to write — options and policy

options:
    enableFileDeletion: false # default: additive restore (don't delete extra files in the target)
    ignorePermissionErrors: true # default true
    writeFilesAtomically: true # default true
    parallel: 4 # restore parallelism; kopia default 8
    skipTimes: true # skip restoring file modification times
    overwriteFiles: true # overwrite existing files in the target
policy:
    onMissingSnapshot: Fail # see table below
    waitTimeout: 5m # how long to wait for the source snapshot to appear

enableFileDeletion makes the target a mirror

By default a restore is additive — it writes the snapshot's files and leaves anything else in the target alone. enableFileDeletion: true deletes files in the target that aren't in the snapshot, making it an exact mirror. Use it deliberately.

options also exposes the rest of kopia snapshot restore's own tuning flags directly, one field per flag: writeSparseFiles, skipOwners, skipPermissions, skipTimes, overwriteFiles, overwriteDirectories, overwriteSymlinks, ignoreErrors, and skipExisting (all tri-state — true/false/absent, where absent means "let kopia decide") plus parallel (a count). See the field reference for kopia's per-flag default.

onMissingSnapshot — fail-closed vs proceed

Value Behavior Default for
Fail No matching snapshot ⇒ the restore fails. snapshotRef / identity (explicit sources).
Continue No matching snapshot ⇒ provision a fresh, empty volume and complete. fromPolicy.

The defaults are the point: an explicit restore that finds nothing is an error you want surfaced; a deploy-or-restore that finds nothing should let the app start with a fresh volume.

On Continue with no snapshot, Kopiur actually provisions the empty volume — it doesn't just mark the Restore complete. For target.populator: {} it provisions an empty prime PVC and rebinds it to the claiming PVC (so a workload pod can bind and start); for target.pvc it creates the empty PVC. The "no snapshot ⇒ empty" decision is pinned to status.resolved (resolution: NoSnapshot) once and never re-resolved, so a snapshot that appears later can never silently restore over a volume the app is already using — re-create the Restore if you want to pick up a new snapshot. The Restore reports Completed with Resolved=True reason=NoSnapshotContinue.

The empty-volume path applies to fromPolicy/identity sources on every backend — the restore Job resolves the source in-place, so an object-store fromPolicy with no snapshot comes up empty under Continue just like a filesystem one.

waitTimeout — wait before giving up

waitTimeout (a Go-style duration, e.g. 5m) opens a grace window, anchored at the Restore's creation, during which "no matching snapshot yet" means wait and re-check instead of giving up. onMissingSnapshot applies only once the window closes. Use it when the Restore may be applied before the thing that produces its snapshot — a schedule about to fire, a GitOps apply ordering, a populator claim racing the first backup.

Where the waiting happens depends on the source. A snapshotRef (waiting for the referenced Snapshot CR to gain an id) re-checks in the controller (~15 s, surfacing Resolved=False reason=WaitingForSnapshot on the conditions). A fromPolicy/identity source re-lists the repository inside the restore Job (the same mover run that does the restore, so it works on every backend) — the Restore shows Restoring for that window rather than a per-poll condition. Either way the window is measured from creation, so it's bounded even across controller restarts or Job pod retries. Because the wait runs inside the Job for the latter, waitTimeout must be shorter than the Job's failurePolicy.activeDeadlineSeconds — the admission webhook rejects a Restore that sets both with waitTimeout ≥ the deadline.

Mover, cache & failure policy

A restore writes data into a PVC, so the mover that does the writing has the same concerns a backup's does. Restore.spec.mover is the same MoverSpec a SnapshotPolicy exposes, and Restore.spec.failurePolicy mirrors Snapshot.spec.failurePolicy. See the full manifest in example 12.

spec:
    mover:
        securityContext: { runAsUser: 1000, runAsGroup: 1000, ... } # CONTAINER: own the restored files
        podSecurityContext: { fsGroup: 1000 } # POD: make a fresh volume writable
        # inheritSecurityContextFrom: { workloadSelector: { podSelector: {...} } }  # ...or copy from a live pod (restore: workloadSelector, not pvcConsumer)
        # inheritSecurityContextFrom: { snapshot: {} }                              # ...or replay the identity RECORDED on the backup (no live pod needed)
        cache: { capacity: 16Gi, mode: Persistent, contentCacheSizeMb: 10000 }
    failurePolicy:
        backoffLimit: 4
        activeDeadlineSeconds: 7200 # cap a RUNNING restore (default 48h backstop)
        podStartupDeadlineSeconds: 300 # fail a restore mover that can't START in 5m (default 300)
  • mover.securityContext — run the restore mover (its container) as the UID/GID that should own the restored files. Without it the mover runs as the hardened default (UID 65532), which may write files the app can't read. This is the fix for "the restore mover had no UID control".
  • mover.podSecurityContext.fsGroup — a pod-level fsGroup that makes a freshly-provisioned target volume group-writable, so an unprivileged runAsUser: 1000 mover can populate it on restore (instead of needing a root mover just to write the new volume). The headline case for restoring into a brand-new PVC as non-root. See Security context → fsGroup.
  • mover.inheritSecurityContextFrom — instead of hard-coding them, copy both the container securityContext and the pod-level securityContext (so the restore mover gets the app's UID and its fsGroup) from somewhere authoritative. On a Restore, two forms work: workloadSelector: { podSelector, container? } names a live pod that will read the restored data, and snapshot: {} replays the identity recorded on the backup itself (Snapshot.status.recorded — no live pod needed; see the re-bootstrap section below). The pvcConsumer form is backup-only — it derives the workload from a backup source PVC, which a restore doesn't have (the target's consumer may not exist yet), so the webhook rejects pvcConsumer on a Restore. Combines with securityContext/podSecurityContext: they are the higher merge layer, so an explicit field overrides the inherited one, and they stand in alone when no workload pod resolves — in which case the restore proceeds on them and reports SecurityContextInherited=False / InheritFallback, because the restored files will then be owned as your context says rather than as the workload you named. The condition RestoreSecurityContextCompatible reports (positively) when the future consumer will be able to read what the mover writes. See Security context → Inherit it from the workload and example 18.
  • mover.cache — size the kopia cache for a large restore. mode: Ephemeral (default) gives a fresh per-run volume sized by capacity (or an emptyDir when unset); mode: Persistent keeps a controller-owned cache PVC and reuses it across runs for a warm cache. contentCacheSizeMb / metadataCacheSizeMb pass kopia's --content/metadata-cache-size-mb budgets. A repository's moverDefaults.cache are inherited and overlaid by mover.cache.
  • failurePolicy — the restore Job's backoffLimit, activeDeadlineSeconds, and podStartupDeadlineSeconds. Absent uses the defaults (2 retries; a 48h activeDeadlineSeconds backstop so a running Job can't linger forever; a 5-minute podStartupDeadlineSeconds so a restore mover that can't start — bad image, unschedulable, impossible securityContext — fails fast with MoverPodWedged instead of hanging). The two deadlines are explained in Backups → failurePolicy.

An elevated restore mover needs the namespace to opt in

A restore mover that runs as root (runAsUser: 0), with added capabilities, or privilegedMode: true — including one inherited from a root workload pod — is refused with MoverPermitted=False until the restore's namespace opts in, exactly like a backup. Opt the namespace in by applying a Namespace carrying the opt-in annotation:

# Privileged-mover namespace opt-in (docs/permissions.md, docs/security-context.md)
#
# A privileged (or root) mover — `runAsUser: 0`, `privileged: true`,
# `allowPrivilegeEscalation: true`, added Linux capabilities, `runAsNonRoot:
# false`, or `privilegedMode: true` — is refused with a `MoverPermitted=False`
# condition until the workload namespace is opted in. This Namespace carries the
# opt-in annotation, so backups, restores, and maintenance in `media` may run an
# elevated mover.
#
# This is the declarative form of the per-namespace admin decision; applying it
# is equivalent to:
#
#   kubectl annotate namespace media kopiur.home-operations.com/privileged-movers=true
#
# Granting a namespace privileged movers is a CLUSTER-ADMIN action: the operator
# mints a mover ServiceAccount in this namespace, and a tenant here could reuse
# it at the mover's privilege. Apply it only for namespaces you trust to run
# elevated workloads. Removing the annotation revokes the opt-in.
---
apiVersion: v1
kind: Namespace
metadata:
  name: media
  annotations:
    # The opt-in. Any other value (or absence) leaves the namespace un-opted-in.
    kopiur.home-operations.com/privileged-movers: "true"
$ kubectl apply -f privileged-mover-namespace.yaml

…or imperatively: kubectl annotate namespace <ns> kopiur.home-operations.com/privileged-movers=true.

See Permissions for how to choose the UID/GID and when a privileged mover is warranted.

Deploy-or-restore (GitOps)

The headline pattern: commit one bundle and apply it to any cluster. On a fresh cluster pointed at an existing repository, the PVC restores the latest snapshot before the app starts; on a brand-new repository, the PVC comes up empty and is backed up going forward. No "is this a new install or a recovery?" branching.

The mechanism is a passive Restore (source.fromPolicy, target.populator: {}, onMissingSnapshot: Continue) consumed by a PVC's dataSourceRef as a volume populator. The full manifest is example 05. The same Restore keeps serving claims for its whole life — tear the app's PVC down and stand it back up (a kubectl delete + re-apply, a namespace rebuild, a migration) and the new PVC is populated again from the pinned snapshot, no change to the Restore needed (see the reusability note above).

Kubernetes ≥ 1.24

The volume-populator handshake relies on the AnyVolumeDataSource feature (GA from 1.24). The optional volume-data-source-validator surfaces a malformed dataSourceRef as an event instead of a silently-stuck PVC.

Restoring a snapshot Kopiur didn't create

Snapshots written by a foreign kopia client, or predating your install, are materialized as discovered Snapshot CRs (origin=discovered, forced deletionPolicy: Retain) in the repository's namespace. Restore them two ways (see example 07):

  • (A) reference the discovered Snapshot CR with source.snapshotRef — same as any other backup; or
  • (B) use source.identity with the raw kopia identity (requires spec.repository), for snapshots that aged out of the catalog.
$ kubectl get snapshots -n backups -l kopiur.home-operations.com/origin=discovered

Snapshots carry their recorded mover identity

Every snapshot Kopiur produces records the resolved mover identity (uid/gid/fsGroup and its provenance) on the snapshot itself as the kopiur-meta tag, and the catalog scan decodes it into status.recorded on discovered rows — so the identity the data expects survives a cluster rebuild with the repository. kubectl kopiur snapshots list -o wide shows it, and a Restore can run as it via inheritSecurityContextFrom: { snapshot: {} } (next section). See Backups → tags.

Declarative re-bootstrap — restore as the recorded identity

After a full cluster loss, the workload whose UID a restore should run as does not exist yet — there is no pod to inherit from. mover.inheritSecurityContextFrom: { snapshot: {} } closes that gap: the restore mover runs as the identity recorded on the backup (Snapshot.status.recorded, decoded from the kopiur-meta kopia tag), with no live pod involved. The whole recovery is then one declarative apply:

  1. GitOps applies Repository + SnapshotPolicy + Restore (source.fromPolicy + snapshot: {}) to the fresh cluster.
  2. The repository connects; the catalog scan materializes discovered Snapshot CRs from the repository, decoding each snapshot's recorded identity into status.recorded.
  3. Until a matching row exists, the Restore holds with SecurityContextInherited=False / MissingRecordedIdentity (a Warning Event says why; it re-checks every few minutes). This is the expected intermediate state, not an error to fix.
  4. The moment the scan lands, the Restore resolves the recorded identity, runs the mover as it, and completes.

No hand-authored snapshot names anywhere: fromPolicy re-resolves the kopia identity from the live SnapshotPolicy and the controller picks the matching Snapshot CR from the catalog (honoring asOf/offset; source.identity works the same way, including a pinned snapshotID). source.snapshotRef also supports snapshot: {} — it reads that CR's status.recorded directly. Pair with target: { populator: {} } for deploy-or-restore: snapshot needs no live pod, so it is the one inherit mode allowed with a populator target.

# Example 37 — Restore as the identity RECORDED on the backup
#
# Every backup records the uid/gid/fsGroup its mover actually ran as on the kopia
# snapshot itself (the `kopiur-meta` tag), surfaced as `Snapshot.status.recorded`.
# `inheritSecurityContextFrom: { snapshot: {} }` makes a Restore's mover run as
# that recorded identity — no live workload pod needed, so it works when the app
# is not deployed yet and after a full cluster re-bootstrap.
#
# Works with EVERY source:
#   - `snapshotRef`          reads the referenced Snapshot CR's status.recorded.
#   - `fromPolicy`/`identity` search the CR catalog by kopia identity, honoring
#     asOf/offset/snapshotID — this is what makes the GitOps re-bootstrap
#     declarative (second Restore below).
#
# Holds and warnings (SecurityContextInherited condition):
#   - `MissingRecordedIdentity` (False): the Snapshot CR is missing, carries no
#     status.recorded (pre-feature or foreign backup), or the catalog scan has
#     not materialized a matching row yet. The Restore holds and re-checks every
#     few minutes; the scan backfills recorded metadata automatically.
#   - `RecordedPinnedNoUid` (False): the backup recorded no pinned uid (its
#     mover's identity was image-determined) — inheriting contributes nothing.
#   - `RecordedApplied` (True): names the Snapshot, the recorded uid, and its
#     provenance (`inherited`/`explicit`/`defaults`).
#
# TRUST BOUNDARY: recorded metadata is repository data — anyone with repository
# write credentials can forge it (including uid 0). A recorded ROOT identity is
# gated on the namespace privileged-movers opt-in exactly like live-pod inherit,
# and the condition/Events always name the recorded uid + snapshot.
#
# Field shapes verified against crates/api:
#   spec.mover.inheritSecurityContextFrom : exactly one of
#       { workloadSelector: {...} } | { pvcConsumer: {...} } | { snapshot: {} }
#   `snapshot` is RESTORE-only (webhook-rejected on SnapshotPolicy/Maintenance).
#
# YAML FOOTGUN: write `snapshot: {}` — a bare `snapshot:` is YAML null and is
# rejected at admission (the variant is an empty sub-object, not a null).
---
# Direct: restore a specific Snapshot as the identity it recorded.
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Restore
metadata:
  name: postgres-restore-as-recorded
  namespace: billing
spec:
  source:
    snapshotRef:
      name: postgres-data-20260523-021300
  target:
    pvc:
      name: postgres-data-restored
      capacity: 100Gi
  mover:
    inheritSecurityContextFrom:
      snapshot: {} # the empty sub-object is required (`snapshot:` alone is null)
---
# Declarative re-bootstrap (GitOps): apply Repository + SnapshotPolicy + this
# Restore together on a fresh cluster. The Restore holds on
# `MissingRecordedIdentity` until the catalog scan materializes a discovered
# Snapshot CR carrying `status.recorded` for the policy's identity, then
# restores the latest backup as the identity it recorded. Pair with
# `target: { populator: {} }` for deploy-or-restore (`snapshot` needs no live
# pod, so it is the one inherit mode allowed with a populator target).
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Restore
metadata:
  name: postgres-rebootstrap
  namespace: billing
spec:
  source:
    fromPolicy:
      name: postgres-data # identity re-resolved from the live SnapshotPolicy
  target:
    pvcRef:
      name: postgres-data # the PVC your app deployment already claims
  mover:
    inheritSecurityContextFrom:
      snapshot: {}

What the mover runs as, the trust boundary for recorded metadata (it is repository data and can be forged — a recorded root identity stays gated on the namespace opt-in), and the full SecurityContextInherited reason table live in Security context → snapshot.

Restoring into a renamed cluster

The catalog scan pre-filters identities that belong to other clusters (foreign hostname suffixes), so a repository restored into a cluster with a different identity scheme may materialize no discovered rows for your old snapshots — and a fromPolicy search then holds forever. The escape hatch is the identity source: give it the old raw kopia triple (username/hostname) and it searches the CR catalog — and selects the snapshot — by exactly that identity, still with snapshot: {}.

asOf selects twice — CR-side identity vs repository-side data

With fromPolicy/identity plus snapshot: {}, data and identity are pinned from the same catalog row: the controller selects one matching Snapshot CR (honoring asOf/offset), pins its kopia snapshot id as the data to restore, and replays that same snapshot's recorded identity — the two can never diverge, so snapshot B's data is never restored under snapshot A's uid/gid/fsGroup. The trade-off is deliberate: selection runs against the CR catalog, not the live repository listing, so under catalog lag the restore pins the newest catalogued snapshot (the scan converges the catalog; a not-yet-catalogued snapshot simply isn't eligible yet). The condition message names the exact Snapshot both came from.

Watching a restore

$ kubectl get restore -n billing -w
NAME              PHASE        AGE
postgres-verify   Resolving    2s
postgres-verify   Restoring    9s
postgres-verify   Completed    41s

Phases: PendingResolving (pinning the source snapshot) → Restoring (mover writing data) → Completed / Failed. Live byte/file progress is in status.progress; the resolved snapshot and target PVC are in status.resolved / status.target. If it won't progress, kubectl describe restore <name> shows the reason on the conditions and as an Event — see Troubleshooting.

Every phase write also carries the kstatus conditions: CompletedReady=True, FailedStalled=True (a Restore is one-shot — fix the cause and create a new Restore), anything in flight ⇒ Reconciling=True. So kubectl wait --for=condition=Ready restore/<name> and Flux/Argo health checks gate on a restore the same way they do on every other kopiur kind, and domain conditions (Resolved, MoverPermitted, CredentialsAvailable, AwaitingClaim) survive phase transitions alongside them.

Credentials in a fresh namespace — credentialProjection

A restore mover loads the repository credentials via envFrom from a Secret in its own namespace. Restoring into a namespace that has never run a backup (disaster recovery, a clone target) won't have one. Set credentialProjection.enabled: true and the operator copies the referenced repository's Secret into the mover's namespace for the run — owned by the Restore, garbage-collected with it (example 17):

spec:
    repository: { kind: ClusterRepository, name: platform-shared }
    credentialProjection:
        enabled: true # off by default; needs Helm features.credentialProjection.enabled

It's off by default (cross-namespace Secret copying is opt-in) and needs the operator's Secret-projection RBAC (Helm features.credentialProjection.enabled). The alternative is placing the Secret in the namespace yourself. See Movers → credential projection.

Field reference — every value, and when to change it

The full Restore surface, with the examples that exercise each. source is the only required field.

Field What it does When to set it
repository The repository to read from ({ kind, name, namespace? }). Inferred from source for snapshotRef/fromPolicy; required for identity. Cross-namespace / cluster restores, or any identity source. (13, 16)
source.snapshotRef Restore a specific Snapshot CR ({ name, namespace? }). The common case — you picked a row from the catalog. (03, 16)
source.fromPolicy Resolve via a SnapshotPolicy's identity ({ name, namespace?, asOf?, offset? }). No Snapshot CR (deploy-or-restore), or point-in-time (asOf) / positional (offset) recovery. (05, 14)
source.identity Raw kopia identity ({ username, hostname, sourcePath?, snapshotID?, asOf?, offset? }). Foreign / aged-out snapshots; needs repository. (13)
target.pvc Create a new PVC and restore into it ({ name, storageClassName?, capacity?, accessModes? }). The safe default — restore beside the original, verify, cut over. (03)
target.pvcRef Restore into an existing PVC ({ name }). In-place restore (scale the app down first). (15)
target.populator Explicit passive volume-populator source (populator: {}). GitOps deploy-or-restore via a PVC dataSourceRef. (05)
options.enableFileDeletion Delete target files not in the snapshot (exact mirror); wired to kopia's --delete-extra. Default false (additive). A faithful in-place restore — destructive, use deliberately. (15)
options.ignorePermissionErrors Complete and report permission problems vs. fail hard. Default true. false to fail-closed when exact permissions matter.
options.writeFilesAtomically Write via a temp file + rename. Default true. Rarely changed.
options.parallel Restore parallelism (--parallel). Kopia default 8. Large restores on fast storage/network.
options.writeSparseFiles / skipOwners / skipPermissions / skipTimes Tri-state passthroughs to kopia's --[no-]write-sparse-files / --[no-]skip-owners / --[no-]skip-permissions / --[no-]skip-times. Absent ⇒ kopia's own default. Sparse-file-heavy targets; cross-platform restores where owners/permissions/times don't translate.
options.overwriteFiles / overwriteDirectories / overwriteSymlinks Tri-state passthroughs to kopia's --[no-]overwrite-* (kopia default true for all three). false to refuse clobbering an existing target.
options.ignoreErrors / skipExisting Tri-state passthroughs to kopia's --[no-]ignore-errors / --[no-]skip-existing. Kopia default false for both. Best-effort restores; resuming a partially-written target.
policy.onMissingSnapshot Fail (explicit sources) vs Continue (fromPolicy default). Fail for deliberate recoveries; Continue for deploy-or-restore.
policy.waitTimeout How long to wait for the source snapshot to appear. Sources that may lag behind the Restore being applied.
mover.securityContext / podSecurityContext Container UID/GID, and the pod-level fsGroup that makes a fresh target volume writable. Own restored files as the app's UID; populate a fresh PVC as non-root (fsGroup). See Mover, cache & failure policy. (12)
mover.cache / resources / inheritSecurityContextFrom Cache sizing/mode, mover resources, inherit-from-pod. Large-restore cache, resource limits, run-as-the-app. (12)
failurePolicy Restore Job backoffLimit / activeDeadlineSeconds. Retry/deadline control for big or flaky restores. (12)
credentialProjection Project the repo Secret into the mover's namespace. Restoring into a fresh namespace from a shared repo. (17)

See also