Skip to main content

restore_source_path

Function restore_source_path 

Source
pub fn restore_source_path(
    policy: &SnapshotPolicy,
    override_: Option<&str>,
    target: &PvcTargetRef,
) -> Result<RestoreSourcePath, ValidationError>
Expand description

The kopia source path a restore of target should read from policy (#443).

This is the cross-volume fix. RestoreSelector.source_path: None becomes the kopia filter username@hostname: — an EMPTY path, which matches every member path of a selector policy — so before this, restoring one PVC of a multi-PVC policy took the newest snapshot of any member and could fill a volume with another volume’s data.

The rule, in order:

  1. override_ (source.fromPolicy.sourcePath) wins outright.
  2. A plain pvc: source addressing EXACTLY this target (same name, same namespace) ⇒ that source’s own path. An exact match beats every derivation AND the first-source fallback.
  3. The policy has no selector sourcesRestoreSourcePath::PolicySource of sources[0]’s own path — byte-identical to what config_identity + resolve_identity produced before this function existed, for the plain pvc:, nfs and sourcePathOverride shapes alike (and None for a zero-source legacy object).
  4. Every selector source agrees on (sourcePathStrategy, sourcePathOverride) and that override is NoneRestoreSourcePath::DerivedFromTarget, built through EffectiveSource::kopia_source_path — the same call the backup side makes, so the two strings cannot drift.
  5. Anything else ⇒ a named error telling the user to set fromPolicy.sourcePath.

(2) is deliberately ahead of (3). Validation admits N plain pvc: sources on one policy (validate::snapshot only requires “at least one”), but today only the FIRST is ever captured: expand_sources returns None unless some source carries a pvcSelector, so a selector-free policy mints one unpinned child and effective_source(policy, None) resolves index 0. (A pre-existing backup-side limitation, tracked separately — not something this function can fix.) That is precisely why the fallback must not answer for every target: sources: [pvc: a, pvc: b] restoring into PVC b used to resolve /pvc/a, a path that is real but holds ANOTHER volume’s data, and filled b with it under a green Completed. With the exact match first, b resolves /pvc/b — never written — so the restore fails honestly with SnapshotNotFound, or comes up empty under Continue. The same applies to [nfs, pvc: a] restoring a, which used to read the NFS export path.

Putting the exact match first is byte-identical for every SINGLE-source shape: a lone plain pvc: source whose name equals the target builds the same EffectiveSource (same index, same PvcTargetRef, same override, same strategy — strategy_for is PvcName for any non-selector source) and so the same string; an nfs source, a differently-named target and a cross-namespace target all miss (2) and fall through to (3) untouched.

A target matching NO plain source still falls back to sources[0] under (3) — e.g. [pvc: a, pvc: b] restoring into a PVC named c reads /pvc/a. That is deliberate, not an oversight: it is the pre-#443 answer, it is what makes “restore this policy’s data into a differently-named scratch volume” keep working, and (5) is reserved for the shapes where a path genuinely cannot be derived (disagreeing selectors, a flattening selector override) — not for a multi-source policy where sources[0] is a defined, if arbitrary, answer. Set fromPolicy.sourcePath to name the member you want.

A selector carrying sourcePathOverride: Some(o) is deliberately NOT per-PVC: kopia_source_path returns the override before it ever looks at the PVC, so every member was backed up under the one path o and no derivation can tell them apart. That falls to (5) rather than silently returning o, because “which member is this?” genuinely has no answer.

Matching is by strategy rule, not by claimant labels: a target.pvc has no labels to match against, and a claimant’s labels may have changed since the backup was taken. This function is therefore pure over the policy + target alone.

The namespace in target is the TARGET’s namespace. For a cross-namespace target.pvcRef under a pvcNamespacedName strategy that derives /pvc/<target-ns>/<name>, which may be a path the repository never saw — use the override there.

let policy: SnapshotPolicy = serde_json::from_value(serde_json::json!({
    "apiVersion": "kopiur.home-operations.com/v1alpha1",
    "kind": "SnapshotPolicy",
    "metadata": { "name": "app", "namespace": "db" },
    "spec": {
        "repository": { "name": "r" },
        "sources": [{
            "pvcSelector": { "matchLabels": { "app": "web" } },
            "sourcePathStrategy": "PvcName",
        }],
    },
}))
.unwrap();
let target = PvcTargetRef { namespace: "db".into(), name: "data-1".into() };
assert_eq!(
    restore_source_path(&policy, None, &target).unwrap(),
    RestoreSourcePath::DerivedFromTarget("/pvc/data-1".into())
);