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:
override_(source.fromPolicy.sourcePath) wins outright.- 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. - The policy has no selector sources ⇒
RestoreSourcePath::PolicySourceofsources[0]’s own path — byte-identical to whatconfig_identity+resolve_identityproduced before this function existed, for the plainpvc:,nfsandsourcePathOverrideshapes alike (andNonefor a zero-source legacy object). - Every selector source agrees on
(sourcePathStrategy, sourcePathOverride)and that override isNone⇒RestoreSourcePath::DerivedFromTarget, built throughEffectiveSource::kopia_source_path— the same call the backup side makes, so the two strings cannot drift. - 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())
);