Skip to content

Scenario 08 — Clone an app's data into another namespace

Reproduce a prod bug against real data, or seed staging. You want production's data in a staging (or preview) namespace — not a hand-exported dump, the actual latest snapshot — without touching the production volume.

The mechanism is a cross-namespace restore: the Restore lives in the destination namespace, while the source reference carries the source namespace. The only wrinkle is credentials — the restore mover runs in the destination, so the repository's credential Secret has to be reachable there.

Step 1 — Make credentials reachable in the destination

The restore mover loads the repo password (and any backend creds) via envFrom from a Secret in its own namespace. A brand-new staging namespace doesn't have one. Two options:

  • Shared ClusterRepository + projection (recommended). With a cluster-scoped repository, set credentialProjection.enabled: true on the Restore and the operator copies the repository's Secret into staging for the run (owned by the Restore, garbage-collected with it). Needs the operator's Secret-projection RBAC (Helm features.credentialProjection.enabled, off by default).
  • Place the Secret yourself. Copy the repo's credential Secret into staging ahead of time and skip credentialProjection.

Step 2 — Restore prod's snapshot into the destination

The bundle creates the staging namespace and a Restore that resolves prod's latest snapshot (source.fromPolicy with namespace: billing) into a new postgres-data-clone PVC in staging. The mover's securityContext matches the staging app's UID so the cloned files are usable. (To clone a specific snapshot instead of the latest, use source.snapshotRef with the prod Snapshot's name + namespace — see example 16.)

# Scenario 08 — Clone an app's data into another namespace (prod → staging)
#
# Reproduce a production bug against REAL data, or seed a staging/preview
# environment, by cloning prod's latest snapshot into another namespace. The
# `snapshotRef` carries the SOURCE namespace (billing); the Restore and its target PVC
# live in the DESTINATION namespace (staging). The restore mover runs in the
# destination, so the repo credentials must be readable there — a shared
# `ClusterRepository` + `credentialProjection` does that without hand-placing
# Secrets.
#
# Field shapes verified against crates/api: source.snapshotRef = { name, namespace? };
# credentialProjection = { enabled }; mover.securityContext is a core/v1
# SecurityContext.
---
# The clone target namespace.
apiVersion: v1
kind: Namespace
metadata:
  name: staging
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Restore
metadata:
  name: clone-prod-postgres
  namespace: staging # DESTINATION
spec:
  repository:
    kind: ClusterRepository # shared, cluster-scoped — reachable from any namespace
    name: platform-shared
  source:
    fromPolicy:
      name: postgres-data
      namespace: billing # SOURCE — resolve prod's latest snapshot by identity
  target:
    pvc:
      name: postgres-data-clone
      storageClassName: standard # staging can use cheaper storage than prod
      capacity: 100Gi
      accessModes:
        - ReadWriteOnce
  # Copy the ClusterRepository's credential Secret into `staging` for this run
  # (owned by this Restore, GC'd with it). Needs Helm features.credentialProjection.enabled.
  credentialProjection:
    enabled: true
  # Run as the staging app's UID so the cloned data is usable.
  mover:
    securityContext:
      runAsUser: 1000
      runAsGroup: 1000
      runAsNonRoot: true
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]
      seccompProfile:
        type: RuntimeDefault
  policy:
    onMissingSnapshot: Fail
    waitTimeout: 5m
#
# Then mount postgres-data-clone in your staging Postgres Deployment/StatefulSet and
# you have prod data in staging. The prod namespace and volume are never touched.
$ kubectl get restore clone-prod-postgres -n staging -w
NAME                  PHASE        AGE
clone-prod-postgres   Resolving    3s
clone-prod-postgres   Restoring    11s
clone-prod-postgres   Completed    52s

Step 3 — Mount the clone

Point your staging Deployment/StatefulSet at postgres-data-clone and you have production data in staging. The production namespace and its volume are never read for write and never modified.

Identity, not magic

fromPolicy resolves the snapshot by kopia identity (<snapshotPolicyName>@<namespace>:/pvc/<pvcName>), which is why it needs the source namespace. The clone lands under a new identity for its own future backups. See How Kopia works.

See also