Every spec and status field of all nine CRDs in kopiur.home-operations.com/v1alpha1, with its type, schema default, and a one-line meaning taken straight from the Rust doc comments.
This page is generated from the kopiur-api CRD schemas by cargo xtask gen-docs (run mise run gen); it is drift-checked in CI, so it can never go stale against the shipped CRDs. To change an entry, edit the doc comment on the field in crates/api — not this file. For the task-oriented explanations (mental model, which knobs you actually change), see the per-CRD pages under CRD reference.
Conventions
Type uses the CRD/YAML shape. enum: A \| B lists the allowed values; []T is an array, map[string]T a map, object (free-form) an unvalidated object, and core/v1 X an embedded Kubernetes type (see the Kubernetes API reference for its fields).
Default is the schema default: the API server materializes when the field is absent. — means no schema default — an optional field that is simply unset, or one whose effective default depends on context (the description says which).
required marks a field with no default that must be present, or admission fails.
Externally-tagged unions select a variant by which key you set, never a kind: field; each is flagged "set exactly one of …".
Validation rules enforced by the API server (x-kubernetes-validations, including immutability) are listed under the table they apply to.
Tuning for the bootstrap/discovery mover Job (<name>-discovery) that connects/creates an object-store repository the operator cannot reach in-process (and re-runs for catalog re-scans).
Identity defaults (CEL *Expr) applied when consumers don't override. Set cluster when this repository's backend is shared across clusters (e.g. one bucket backed up from more than one Kubernetes cluster) — the default kopia identity hostname then becomes <namespace>.<cluster> instead of bare <namespace>, so same-named namespaces on different clusters never collide. Same semantics as ClusterRepository's field of the same name (ADR-0004 §5); a consumer's explicit spec.identity still wins over anything here.
Scheduling defaults (timezone, jitter) inherited by consumers that don't set their own equivalent field — backup, verification, replication, and maintenance schedules; set once here instead of repeating it on every cron.
Initialize this repository from an existing replica on its FIRST bootstrap (issue #380) — a disaster-recovery entry point. Armed only while the repository has never been initialized (status.uniqueId unset) and the mover's connect reports the backend uninitialized; on an already-initialized repository it is a documented no-op (Seeded=True, reason AlreadyInitialized), so it is safe to leave standing in a GitOps manifest. When armed it also replaces spec.create's fallback: the repository is seeded or the bootstrap fails, never silently created empty.
Google Drive via kopia's native gdrive provider (service-account JSON). kopia marks this provider experimental / not maintained, and a native gdrive repository is not interchangeable with an rclone-backed Drive remote.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Secret holding the Google service-account JSON used to reach the folder, read by the well-known key KOPIA_GDRIVE_CREDENTIALS. Absent means kopia falls back to ambient credentials (GOOGLE_APPLICATION_CREDENTIALS or instance metadata).
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Secret holding the rclone.conf that defines the remote referenced by remote_path.
startupTimeout
string
—
How long kopia waits for its embedded rclone serve to come up before failing the connect, as a Go duration (e.g. 2m). kopia's default is 15s; raise it for slow remotes whose repository metadata/indexes load through the rclone/WebDAV bridge and take longer than the default budget.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Name of the ConfigMap holding the value (e.g. a CA bundle). Resolved in the referrer's namespace for a namespaced Repository, and in the operator's namespace (KOPIUR_NAMESPACE) for a ClusterRepository (cluster-scoped, no namespace of its own).
key
string
—
Which key inside the ConfigMap to read; defaults to ca.crt when unset.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Failure policy for the bootstrap Job. activeDeadlineSeconds caps how long a connect may run before the Job is marked failed (default 120s); raise it for a slow backend — e.g. an rclone remote whose repository metadata and indexes load through kopia's embedded rclone serve/WebDAV bridge, or a large repository whose cold-cache connect outgrows the default. The value is a BASE, not a ceiling: after consecutive deadline-killed attempts the operator escalates the effective deadline itself (doubling per attempt, up to 30 minutes or the configured value, whichever is larger — never below it), so a slow-but-alive backend self-heals without a spec edit. backoffLimit bounds retries. podStartupDeadlineSeconds is accepted for shape parity but is not honored by the bootstrap Job.
Whether a discovered snapshot whose resolved identity matches a live SnapshotPolicy is automatically adopted — re-attached (stamped with that policy's config label, status.origin flipped to Adopted) so GFS retention governs it and eventually prunes it, instead of it sitting in the catalog forever as an immortal discovered row.
fallbackNamespace
string
—
Where to materialize discovered Snapshots with no allowed-namespace mapping (ClusterRepository only).
foreignSnapshots
enum: Ignore | Fallback
—
How catalog discovery treats snapshots written by ANOTHER cluster. status.catalog.foreignSnapshotCount counts an identity hostname carrying a different .<cluster> suffix ALWAYS, under either value below, plus — under Ignore only — a bare hostname naming no allowed namespace here. Under Fallback, that same disallowed bare host is NOT counted foreign: it materializes into catalog.fallbackNamespace exactly like a disallowed OwnCluster host would, so it is placed rather than dropped-and-counted (see classify_hostname/decide_cluster_placement). Meaningful only when identityDefaults.cluster is set: without a cluster identity there is no notion of "foreign" and the legacy hostname-names-a-namespace placement applies (validators enforce this).
periodicRefresh
boolean
—
Opt-in: periodically re-scan the repository to keep discovered Snapshot CRs current (re-list snapshots; for object-store / volume-backed repos this recycles the <name>-discovery mover Job every refreshInterval). Off by default — the repository still bootstraps once, re-bootstraps on a spec change, and re-probes on a backup failure, but does not re-run on a timer. Enable it if you rely on discovered snapshots reflecting changes made outside this operator.
refreshInterval
string
1h
How often to re-scan when periodicRefresh: true (Go-style duration; minimum 30s, default 1h). Inert unless periodicRefresh is enabled.
Ceiling on how many of this repository's mover Jobs may be in flight at once. Backup snapshots, restores, and replication runs that READ FROM this repository all draw from ONE pool — a repository's backend (and the bandwidth to it) is the shared resource, so splitting the budget per work kind would let three "safe" limits still saturate it. Restores are always admitted and never parked: a restore is a recovery in progress, and holding one behind a queue of routine backups is exactly backwards. An in-flight restore still COUNTS toward the pool, so it displaces backups rather than adding to them. Excluded from the pool entirely: maintenance (already single-flight per repository), verification, pin, and snapshot-delete batch Jobs. These are operator-driven housekeeping that must not be starved by a saturated backup pool. Absent or 0 means unlimited — the default, and today's behavior. No schema default: is emitted for this field (api-conventions §4a): absent and 0 are the SAME state (unlimited), so a materialized default would stamp {maxConcurrentJobs: 0} onto every stored repository — GitOps diff noise for a value that changes nothing.
Reed-Solomon ECC parity for a freshly-created repository (creation-time only, immutable after).
enabled
boolean
true
Create the repository if it does not exist yet — on by default. Repository create/connect is idempotent: pointing create at an already-initialized repository just connects to it (it never re-creates or clobbers), so the only effect of the default is that a genuinely-absent repository is bootstrapped instead of erroring. Set false for a strictly read-only or externally-managed repository the operator must never create.
encryption
string
—
kopia encryption algorithm for a freshly-created repository (creation-time only).
hash
string
—
kopia content hash algorithm for a freshly-created repository (creation-time only).
splitter
string
—
kopia object splitter for a freshly-created repository (creation-time only).
Pending EXTERNAL destructive Snapshot deletions (deletionTimestamp set, effective deletionPolicy Delete, not operator-pruned) that trip the breaker for this repository: at or above this, those deletions are HELD (finalizers wait) until acknowledged via the kopiur.home-operations.com/allow-mass-deletion annotation. 0 disables the breaker. Default 10.
Periodic backend health probe (on by default): re-connect the repository on a timer to confirm the kopia repository still exists at the backend. With the default onFailure: Degrade it doubles as the repository circuit breaker — see probe.onFailure. Disable with probe.enabled: false.
Whether the probe runs (default true). false disables probing — and with it the circuit breaker, since the probe is its only sensor: a wiped or unreachable backend then goes unnoticed until the next backup fails.
failureThreshold
integer
3
How many consecutive failing probes to require before the failure is acted on (default 3): the loud BackendReachable=False condition + event fire, and — under onFailure: Degrade — the repository moves to Degraded. Debounces a single transient blip from alarming, tripping the breaker, or nudging a destructive manual recreate. Any success resets it.
interval
string
30m
How often to re-probe the backend (Go-style duration like 30m or 1h; minimum 30s, default 30m). Inert when enabled: false.
onFailure
enum: Degrade | Alert
Degrade
What sustained backend-probe failure (past failureThreshold) does to the repository (default Degrade). Degrade moves it to Degraded, pausing backups and replication until a re-connect succeeds — recovery is automatic (maintenance also pauses for a confirmed-unreachable/vanished backend, but keeps running for a probe deadline kill, where index compaction is often the cure). Alert keeps the repository Ready and only raises the BackendReachable condition + Warning event + metric; backups keep running against the failing backend. Neither ever auto-recreates.
This cluster's identity suffix for repositories shared across clusters (an RFC 1123 label, at most 32 characters; dots are rejected — the first . in a hostname is the namespace/cluster delimiter). When set, the default kopia identity hostname becomes <namespace>.<cluster> instead of <namespace>, so two clusters backing up same-named namespaces write distinct identities (and one cluster's retention prune can no longer touch the other's snapshots). Also exposed to hostnameExpr/ usernameExpr as the CEL variable cluster.
hostnameExpr
string
—
CEL expression for the kopia identity hostname (e.g. "namespace").
usernameExpr
string
—
CEL expression for the kopia identity username (e.g. "namespace + '-' + policyName").
Copy the UID/GID security context from a live workload rather than hard-coding it. Requires the workload to pin runAsUser (container or pod level): a UID that comes from the container image's USER line is invisible in the pod spec and cannot be inherited — the mover would silently run as its own image's UID instead. May be combined with securityContext/podSecurityContext, which override it field-wise and act as the fallback when no workload pod can be resolved.
podSecurityContext
core/v1 PodSecurityContext
—
Pod security context for the mover (notably fsGroup for group-writable restore volumes). Same layering as securityContext: highest layer, merged field-wise, and combinable with inheritSecurityContextFrom.
privilegedMode
boolean
—
Opt-in, namespace-gated privileged mode; preserves UID/GID on restore.
resources
core/v1 ResourceRequirements
—
Resource requests/limits for the mover container.
securityContext
core/v1 SecurityContext
—
Container security context for the mover; merged field-wise over the hardened base, moverDefaults, and any inherited context — this is the highest layer, so every field set here wins. Combines with inheritSecurityContextFrom: fields you set override the workload's, fields you omit are inherited, and this context stands in alone when inheritance cannot resolve a pod.
ttlSecondsAfterFinished
integer
—
Per-recipe override of Job.spec.ttlSecondsAfterFinished so finished Jobs self-GC.
Backup sources only: auto-derive the workload from the PVC this snapshot backs up.
snapshot
object
—
Restores only: inherit the identity RECORDED on the backup itself (Snapshot.status.recorded, decoded from the kopiur-meta kopia tag) — uid/gid/fsGroup the backup mover actually ran as. Needs no live workload pod, so it works on a rebuilt cluster and with target.populator. Rejected at admission on SnapshotPolicy/Maintenance (backups read the live workload; maintenance has no snapshot). Write it as snapshot: {} (an empty sub-object) — a bare snapshot: is null and rejected.
Extra annotations for every mover pod. Unlike podLabels these are pod-template-only — they are not mirrored onto the Job object, because the common case is a sidecar-injection opt-out (sidecar.istio.io/inject: "false", linkerd.io/inject: disabled, vault.hashicorp.com/agent-inject: "false") that only means anything on the pod a mesh webhook actually sees. A mover is a short-lived batch pod; an injected sidecar that never exits keeps its Job running forever. Same reserved keys as podLabels, rejected at admission.
podLabels
map[string]string
—
Extra labels for every mover POD (and the Job that owns it), merged UNDER kopiur's own labels — a key kopiur sets always wins, so a user-supplied value can never break the selectors the controller counts and reaps by. This is the hook for cluster machinery that keys off pod labels and that kopiur has no field of its own for: a Kueue kueue.x-k8s.io/queue-name to put movers under a cluster queue, a monitoring/NetworkPolicy selector, a service-mesh exclusion label. Keys under kopiur.home-operations.com/ and the exact key app.kubernetes.io/managed-by are reserved and rejected at admission.
podSecurityContext
core/v1 PodSecurityContext
—
Pod security-context base (notably fsGroup) for every mover.
resources
core/v1 ResourceRequirements
—
Resource requests/limits base for the mover container.
COMPLIANCE — nobody can shorten or remove the lock before it expires, including the account root. An oversized period is an unfixable storage-cost commitment; there is no recovery path short of deleting the bucket after expiry.
disabled
boolean
—
Actively disable retention (--retention-mode=none). Must be true. A bool rather than a unit variant because an externally-tagged unit variant serializes as the bare string "Disabled", mixing string and object forms in one oneOf and breaking the structural schema. Same shape, and same reason, as crate::cluster_repository::AllowedNamespaces::All. This is distinct from omitting blobRetention entirely: absent means "leave the repository alone", so deleting the block from a manifest can never silently strip ransomware protection someone configured deliberately.
GOVERNANCE — locked against ordinary deletes, but a sufficiently privileged identity can still shorten or remove the lock. The safe default for most clusters.
Go-style duration; kopia's minimum is 24h and there is no maximum. Accepts h/m/s (or a bare number of seconds) — not d. Note that kopia's own CLI does take 30d, so a period copied from kopia's documentation is rejected here; write 30 days as 720h. Kopiur deliberately keeps one duration grammar across every CRD field rather than matching kopia's per-flag variations.
Go-style duration; kopia's minimum is 24h and there is no maximum. Accepts h/m/s (or a bare number of seconds) — not d. Note that kopia's own CLI does take 30d, so a period copied from kopia's documentation is rejected here; write 30 days as 720h. Kopiur deliberately keeps one duration grammar across every CRD field rather than matching kopia's per-flag variations.
Index blobs in an epoch that trigger an advance, once older than minDuration (kopia default 20).
advanceOnSizeMiB
integer
—
Total index size in an epoch that triggers an advance, once older than minDuration (kopia default 10 MiB). Named MiB, not MB, with an explicit rename rather than the derived camelCase (advanceOnSizeMb, which reads as megabit). The unit is genuinely mebibytes — kopia's --epoch-advance-on-size-mb multiplies by 1048576, so 10 is 10485760 bytes — even though kopia's own log renders the result as "MB". That ambiguity is this field's main hazard; the API surface should not reproduce it.
checkpointFrequency
integer
—
Epochs between full index checkpoints (kopia default 7).
deleteParallelism
integer
—
Parallelism for epoch cleanup deletions (kopia default 4).
minDuration
string
—
Minimum epoch age before it may advance (kopia default 24h). A Go-style duration (6h, 90m). The advance gate — no blob count closes an epoch younger than this.
refreshFrequency
string
—
How often clients re-read epoch state (kopia default 20m). Go-style duration.
Deterministic jitter window (Go-style duration, e.g. 10m) applied to every consuming cron that doesn't set its own jitter — SnapshotSchedule, Maintenance (quick and full), SnapshotPolicy verification (quick and deep), and both replication kinds. Set once here instead of repeating it on every cron, so a whole repository's schedules spread their load off the top-of-the-hour thundering herd. The spread is deterministic per (scheduleUID, slot), not random: the same slot always lands on the same instant, so a restart never re-rolls it. Capped at 24h by validation — jitter is a spread WITHIN a cron period, not a schedule offset.
timezone
string
—
IANA timezone name applied to every consuming cron that doesn't set its own timezone (e.g. America/New_York). Set once here instead of repeating it on every SnapshotPolicy.verification, RepositoryReplication.schedule, and Maintenance.schedule cron.
Where the seed data comes from: exactly one of a bare storage backend (blob mode) or another repository CR (migrate mode).
allowEmptySource
boolean
—
Accept a source that holds zero snapshots (default false). A mirror that answers but is empty is almost always a mis-pointed bucket/prefix, and silently seeding nothing would hand you a Ready repository with no history — the failure mode #380 is about. By default the bootstrap fails loudly and retries; set this when an empty source is genuinely expected (e.g. re-homing a repository whose history was deliberately pruned to nothing).
Opt-in projection of the SOURCE repository's credential Secrets into the seeding mover Job's namespace. Migrate mode only in practice — a blob-mode source's credentials must already be in the namespace the bootstrap Job runs in (this CR's own namespace for a Repository; for a ClusterRepository the operator's namespace, unless encryption.passwordSecretRef.namespace pins another, in which case the Job runs there). Requires the operator's features.credentialProjection install flag.
Deadline/backoff for the seeding bootstrap Job. An absent activeDeadlineSeconds means 24h here, rather than the 120s a routine connect gets — a seed copies a whole repository, once. Only applied while the seed is armed; later connects to the now-initialized repository use spec.bootstrap.failurePolicy as before.
A bare storage backend holding a byte-for-byte mirror of a kopia repository (blob mode, kopia repository sync-to). The mirror's format and encryption password are inherited verbatim, so this repository's encryption.passwordSecretRef must already hold the MIRROR's password and spec.create's format knobs are refused as inert.
Another Repository/ClusterRepository, opened read-only (migrate mode, kopia snapshot migrate). Snapshot identities and times are preserved, so seeded history is restorable by identity/fromPolicy. A kind: Repository reference with no namespace resolves in the referrer's own namespace — and, on a cluster-scoped ClusterRepository (which has none), in the operator's namespace, the same rule its credential secretRefs follow. Set namespace explicitly whenever the source lives anywhere else.
Google Drive via kopia's native gdrive provider (service-account JSON). kopia marks this provider experimental / not maintained, and a native gdrive repository is not interchangeable with an rclone-backed Drive remote.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Secret holding the Google service-account JSON used to reach the folder, read by the well-known key KOPIA_GDRIVE_CREDENTIALS. Absent means kopia falls back to ambient credentials (GOOGLE_APPLICATION_CREDENTIALS or instance metadata).
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Secret holding the rclone.conf that defines the remote referenced by remote_path.
startupTimeout
string
—
How long kopia waits for its embedded rclone serve to come up before failing the connect, as a Go duration (e.g. 2m). kopia's default is 15s; raise it for slow remotes whose repository metadata/indexes load through the rclone/WebDAV bridge and take longer than the default budget.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Name of the ConfigMap holding the value (e.g. a CA bundle). Resolved in the referrer's namespace for a namespaced Repository, and in the operator's namespace (KOPIUR_NAMESPACE) for a ClusterRepository (cluster-scoped, no namespace of its own).
key
string
—
Which key inside the ConfigMap to read; defaults to ca.crt when unset.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Copy only each source identity's most recent snapshot instead of its full history (kopia snapshot migrate --latest). Default false — a seed exists to recover history, so the full copy is the sane default; set this when you only need the latest restore point back quickly.
parallel
integer
— min 0
--parallel: snapshots migrated concurrently (kopia default 1 — sequential). Must be >= 1 when set.
policies
enum: none | copy | copyOverwrite
none
Whether the source's kopia policies are copied along with the snapshots. Defaults to PolicyCopyMode::None (an explicit --no-policies), not kopia's own copy-by-default: retention in a kopiur-managed repository is driven by Snapshot CRs, and importing the source's kopia-side policies could delete manifests behind the operator's back.
Bandwidth/ops caps for THIS seed's copy, per side. A migrate seed opens two repositories under two kopia connections and snapshot migrate has no speed flags of its own, so each side is applied as kopia repository throttle set on that side's connection: source caps the REPLICA — the repository named by spec.seed.from.repository, opened read-only — overriding itsmoverDefaults.throttle; * destination caps THIS repository, the one being seeded, overriding its own* moverDefaults.throttle. Each side overrides field by field: a knob set here wins, a knob left unset keeps that repository's default. Absent: both sides use their repository's defaults. Applies only while the seed is armed — an ordinary connect to the now-initialized repository is capped by moverDefaults.throttle alone.
Caps for the DESTINATION (write) side, overriding the destination repository's moverDefaults.throttle field by field. Applied with repository throttle set on the destination connection the migrate writes through.
Caps for the SOURCE (read) side, overriding the source repository's moverDefaults.throttle field by field. Applied with repository throttle set on the migrate's read-only source connection — accepted there, so a read-only source is throttled like any other.
--max-download-speed: cap read throughput from the seed source, in bytes/sec (kopia default: unlimited).
maxUploadSpeedBytesPerSecond
integer
—
--max-upload-speed: cap write throughput into this repository, in bytes/sec (kopia default: unlimited).
parallel
integer
— min 0
--parallel: concurrent blob-copy workers (kopia default 1 — sequential). Raise it: a first-time seed of a large repository over a WAN is exactly the workload sequential copying is worst at.
Repository size and snapshot counts from the last catalog scan.
uniqueId
string
—
Kopia repository unique ID, pinned on the first successful bootstrap. Its presence is the "this repository has been Ready" flag that makes auto-create one-way in time: spec.create.enabled governs the FIRST bootstrap only, and once this is set kopiur will never create a fresh empty repository over the backend, however empty the backend goes. A wiped backend therefore parks at Failed with reason RepositoryReinitializeBlocked instead of being silently re-created. To deliberately re-initialize a wiped repository, annotate it with kopiur.home-operations.com/allow-reinitialize set to THIS value; the ack is honored only while it matches, so the new ID minted by a successful re-initialize makes it inert. This discards the history the old repository held.
How many Snapshot CRs were materialized from the catalog scan.
foreignSnapshotCount
integer
—
Snapshots in the last complete listing classified as another cluster's (see catalog.foreignSnapshots); never materialized under Ignore. As of catalog.lastRefreshAt — enable periodicRefresh to keep it current.
lastRefreshAt
string
—
RFC 3339 timestamp of the last catalog refresh.
scanRequestAttemptAt
string
—
RFC 3339 timestamp of the last bootstrap/scan attempt initiated BECAUSE OF a pending catalog-scan-requested-at token (i.e. the token arm was the reason the attempt fired). Used only to rate-limit token-driven attempts on a Ready-but-unreachable repository — it is never compared against the token for retirement (that's scanRequestHonored, by equality).
scanRequestHonored
string
—
The RFC3339 token VALUE of the kopiur.home-operations.com/catalog-scan-requested-at annotation last honored by a completed catalog scan. Compared by equality against the live annotation to decide whether a requested on-demand scan is still pending — deliberately NOT a timestamp comparison against lastRefreshAt (a periodic refresh completing after the request was made would otherwise look like it honored the request even though it started before the annotation was set).
lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
message
string
required
message is a human readable message indicating details about the transition. This may be an empty string.
reason
string
required
reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
status
string
required
status of the condition, one of True, False, Unknown.
type
string
required
type of condition in CamelCase or in foo.example.com/CamelCase.
observedGeneration
integer
—
observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditionsx.observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
Consecutive failing probes accrued; reset to zero on any success. The loud condition is raised only once this reaches the failure threshold.
firstFailureAt
string
—
RFC 3339 timestamp of the first failure in the current failing streak.
lastHealthyAt
string
—
RFC 3339 timestamp of the last successful probe (backend reachable, repo present).
lastProbeAt
string
—
RFC 3339 timestamp of the last completed probe (success or failure); drives the health_probe_due timer so the probe re-fires on cadence.
probeAttemptAt
string
—
RFC 3339 timestamp at which the last backend health probe was launched (the bootstrap Job created for it), cleared when its result is finalized. This is the launch-side rate limit, and it is what makes the probe terminate. lastProbeAt is written only at finalize, so gating the launch on that alone recycles the bootstrap Job forever: the gate destroys every Job whose completion would have cleared it (#273). Never compared against lastProbeAt — see kopiur_controller::health::probe_action.
Which copy mechanism a seed ran. Mirrors the SeedSource variant, named after the operation rather than the input so status, metrics (kopiur_repository_seed_total{mode}) and docs share one vocabulary.
seededAt
string
—
RFC 3339 timestamp the seed completed. Set once: a repository is seeded exactly once, at its first bootstrap.
snapshotCount
integer
—
Snapshots observed at the SOURCE when the seed ran. Zero is only ever recorded when allowEmptySource permitted it.
snapshotsCopied
integer
—
Snapshots actually copied into this repository. Migrate mode only — a blob copy moves storage, not manifests, so there is no per-snapshot copy count to report and it leaves this unset; its snapshotCount is the listing taken at the SOURCE before the copy, which for a byte-for-byte mirror is also what this repository ends up holding.
source
string
—
The source the data came from, rendered by SeedSource::describe — the backend discriminant (S3, Filesystem, …) for blob mode, Kind/name for migrate mode. Never a credential or a bucket path.
startedAt
string
—
RFC 3339 timestamp the operator LAUNCHED a seeding bootstrap Job for this repository — the durable seed-attempt marker. Stamped before the Job is created, and never cleared. Its whole job is to distinguish "a seed this operator started did not finish" from "this backend was initialized by somebody else": the first must resume the copy, the second must keep the no-clobber AlreadyInitialized path. See seed_resume — the marker is the ONLY input the resume decision is allowed to take, because a resuming migrate writes into whatever repository is at the backend and then re-stamps its maintenance owner.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Number of content-index blobs (kopia index list) observed at the last bootstrap.
lastObservedAt
string
—
RFC 3339 timestamp these stats were last observed.
snapshotCount
integer
—
Total snapshots present in the repository (across all identities).
totalSize
string
—
Human-readable total on-disk size (e.g. 412Gi).
totalSizeBytes
integer
—
Logical bytes under management (the integer form of total_size): the sum, over each distinct snapshot source, of the most-recent snapshot's logical size. Exposed to backup preflight as repository.sizeBytes. This is repository total size, not backend free space (object stores don't report remaining capacity).
Tuning for the bootstrap/discovery mover Job (<name>-discovery) that connects/creates an object-store repository the operator cannot reach in-process (and re-runs for catalog re-scans).
Scheduling defaults (timezone, jitter) inherited by consumers that don't set their own equivalent field — backup, verification, replication, and maintenance schedules; set once here instead of repeating it on every cron.
Initialize this repository from an existing replica on its FIRST bootstrap (issue #380) — a disaster-recovery entry point. Armed only while the repository has never been initialized (status.uniqueId unset) and the mover's connect reports the backend uninitialized; on an already-initialized repository it is a documented no-op (Seeded=True, reason AlreadyInitialized), so it is safe to leave standing in a GitOps manifest. When armed it also replaces spec.create's fallback: the repository is seeded or the bootstrap fails, never silently created empty.
Google Drive via kopia's native gdrive provider (service-account JSON). kopia marks this provider experimental / not maintained, and a native gdrive repository is not interchangeable with an rclone-backed Drive remote.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Secret holding the Google service-account JSON used to reach the folder, read by the well-known key KOPIA_GDRIVE_CREDENTIALS. Absent means kopia falls back to ambient credentials (GOOGLE_APPLICATION_CREDENTIALS or instance metadata).
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Secret holding the rclone.conf that defines the remote referenced by remote_path.
startupTimeout
string
—
How long kopia waits for its embedded rclone serve to come up before failing the connect, as a Go duration (e.g. 2m). kopia's default is 15s; raise it for slow remotes whose repository metadata/indexes load through the rclone/WebDAV bridge and take longer than the default budget.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Name of the ConfigMap holding the value (e.g. a CA bundle). Resolved in the referrer's namespace for a namespaced Repository, and in the operator's namespace (KOPIUR_NAMESPACE) for a ClusterRepository (cluster-scoped, no namespace of its own).
key
string
—
Which key inside the ConfigMap to read; defaults to ca.crt when unset.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Failure policy for the bootstrap Job. activeDeadlineSeconds caps how long a connect may run before the Job is marked failed (default 120s); raise it for a slow backend — e.g. an rclone remote whose repository metadata and indexes load through kopia's embedded rclone serve/WebDAV bridge, or a large repository whose cold-cache connect outgrows the default. The value is a BASE, not a ceiling: after consecutive deadline-killed attempts the operator escalates the effective deadline itself (doubling per attempt, up to 30 minutes or the configured value, whichever is larger — never below it), so a slow-but-alive backend self-heals without a spec edit. backoffLimit bounds retries. podStartupDeadlineSeconds is accepted for shape parity but is not honored by the bootstrap Job.
Whether a discovered snapshot whose resolved identity matches a live SnapshotPolicy is automatically adopted — re-attached (stamped with that policy's config label, status.origin flipped to Adopted) so GFS retention governs it and eventually prunes it, instead of it sitting in the catalog forever as an immortal discovered row.
fallbackNamespace
string
—
Where to materialize discovered Snapshots with no allowed-namespace mapping (ClusterRepository only).
foreignSnapshots
enum: Ignore | Fallback
—
How catalog discovery treats snapshots written by ANOTHER cluster. status.catalog.foreignSnapshotCount counts an identity hostname carrying a different .<cluster> suffix ALWAYS, under either value below, plus — under Ignore only — a bare hostname naming no allowed namespace here. Under Fallback, that same disallowed bare host is NOT counted foreign: it materializes into catalog.fallbackNamespace exactly like a disallowed OwnCluster host would, so it is placed rather than dropped-and-counted (see classify_hostname/decide_cluster_placement). Meaningful only when identityDefaults.cluster is set: without a cluster identity there is no notion of "foreign" and the legacy hostname-names-a-namespace placement applies (validators enforce this).
periodicRefresh
boolean
—
Opt-in: periodically re-scan the repository to keep discovered Snapshot CRs current (re-list snapshots; for object-store / volume-backed repos this recycles the <name>-discovery mover Job every refreshInterval). Off by default — the repository still bootstraps once, re-bootstraps on a spec change, and re-probes on a backup failure, but does not re-run on a timer. Enable it if you rely on discovered snapshots reflecting changes made outside this operator.
refreshInterval
string
1h
How often to re-scan when periodicRefresh: true (Go-style duration; minimum 30s, default 1h). Inert unless periodicRefresh is enabled.
Ceiling on how many of this repository's mover Jobs may be in flight at once. Backup snapshots, restores, and replication runs that READ FROM this repository all draw from ONE pool — a repository's backend (and the bandwidth to it) is the shared resource, so splitting the budget per work kind would let three "safe" limits still saturate it. Restores are always admitted and never parked: a restore is a recovery in progress, and holding one behind a queue of routine backups is exactly backwards. An in-flight restore still COUNTS toward the pool, so it displaces backups rather than adding to them. Excluded from the pool entirely: maintenance (already single-flight per repository), verification, pin, and snapshot-delete batch Jobs. These are operator-driven housekeeping that must not be starved by a saturated backup pool. Absent or 0 means unlimited — the default, and today's behavior. No schema default: is emitted for this field (api-conventions §4a): absent and 0 are the SAME state (unlimited), so a materialized default would stamp {maxConcurrentJobs: 0} onto every stored repository — GitOps diff noise for a value that changes nothing.
Reed-Solomon ECC parity for a freshly-created repository (creation-time only, immutable after).
enabled
boolean
true
Create the repository if it does not exist yet — on by default. Repository create/connect is idempotent: pointing create at an already-initialized repository just connects to it (it never re-creates or clobbers), so the only effect of the default is that a genuinely-absent repository is bootstrapped instead of erroring. Set false for a strictly read-only or externally-managed repository the operator must never create.
encryption
string
—
kopia encryption algorithm for a freshly-created repository (creation-time only).
hash
string
—
kopia content hash algorithm for a freshly-created repository (creation-time only).
splitter
string
—
kopia object splitter for a freshly-created repository (creation-time only).
Pending EXTERNAL destructive Snapshot deletions (deletionTimestamp set, effective deletionPolicy Delete, not operator-pruned) that trip the breaker for this repository: at or above this, those deletions are HELD (finalizers wait) until acknowledged via the kopiur.home-operations.com/allow-mass-deletion annotation. 0 disables the breaker. Default 10.
Periodic backend health probe (on by default): re-connect the repository on a timer to confirm the kopia repository still exists at the backend. With the default onFailure: Degrade it doubles as the repository circuit breaker — see probe.onFailure. Disable with probe.enabled: false.
Whether the probe runs (default true). false disables probing — and with it the circuit breaker, since the probe is its only sensor: a wiped or unreachable backend then goes unnoticed until the next backup fails.
failureThreshold
integer
3
How many consecutive failing probes to require before the failure is acted on (default 3): the loud BackendReachable=False condition + event fire, and — under onFailure: Degrade — the repository moves to Degraded. Debounces a single transient blip from alarming, tripping the breaker, or nudging a destructive manual recreate. Any success resets it.
interval
string
30m
How often to re-probe the backend (Go-style duration like 30m or 1h; minimum 30s, default 30m). Inert when enabled: false.
onFailure
enum: Degrade | Alert
Degrade
What sustained backend-probe failure (past failureThreshold) does to the repository (default Degrade). Degrade moves it to Degraded, pausing backups and replication until a re-connect succeeds — recovery is automatic (maintenance also pauses for a confirmed-unreachable/vanished backend, but keeps running for a probe deadline kill, where index compaction is often the cure). Alert keeps the repository Ready and only raises the BackendReachable condition + Warning event + metric; backups keep running against the failing backend. Neither ever auto-recreates.
This cluster's identity suffix for repositories shared across clusters (an RFC 1123 label, at most 32 characters; dots are rejected — the first . in a hostname is the namespace/cluster delimiter). When set, the default kopia identity hostname becomes <namespace>.<cluster> instead of <namespace>, so two clusters backing up same-named namespaces write distinct identities (and one cluster's retention prune can no longer touch the other's snapshots). Also exposed to hostnameExpr/ usernameExpr as the CEL variable cluster.
hostnameExpr
string
—
CEL expression for the kopia identity hostname (e.g. "namespace").
usernameExpr
string
—
CEL expression for the kopia identity username (e.g. "namespace + '-' + policyName").
Copy the UID/GID security context from a live workload rather than hard-coding it. Requires the workload to pin runAsUser (container or pod level): a UID that comes from the container image's USER line is invisible in the pod spec and cannot be inherited — the mover would silently run as its own image's UID instead. May be combined with securityContext/podSecurityContext, which override it field-wise and act as the fallback when no workload pod can be resolved.
podSecurityContext
core/v1 PodSecurityContext
—
Pod security context for the mover (notably fsGroup for group-writable restore volumes). Same layering as securityContext: highest layer, merged field-wise, and combinable with inheritSecurityContextFrom.
privilegedMode
boolean
—
Opt-in, namespace-gated privileged mode; preserves UID/GID on restore.
resources
core/v1 ResourceRequirements
—
Resource requests/limits for the mover container.
securityContext
core/v1 SecurityContext
—
Container security context for the mover; merged field-wise over the hardened base, moverDefaults, and any inherited context — this is the highest layer, so every field set here wins. Combines with inheritSecurityContextFrom: fields you set override the workload's, fields you omit are inherited, and this context stands in alone when inheritance cannot resolve a pod.
ttlSecondsAfterFinished
integer
—
Per-recipe override of Job.spec.ttlSecondsAfterFinished so finished Jobs self-GC.
Backup sources only: auto-derive the workload from the PVC this snapshot backs up.
snapshot
object
—
Restores only: inherit the identity RECORDED on the backup itself (Snapshot.status.recorded, decoded from the kopiur-meta kopia tag) — uid/gid/fsGroup the backup mover actually ran as. Needs no live workload pod, so it works on a rebuilt cluster and with target.populator. Rejected at admission on SnapshotPolicy/Maintenance (backups read the live workload; maintenance has no snapshot). Write it as snapshot: {} (an empty sub-object) — a bare snapshot: is null and rejected.
Extra annotations for every mover pod. Unlike podLabels these are pod-template-only — they are not mirrored onto the Job object, because the common case is a sidecar-injection opt-out (sidecar.istio.io/inject: "false", linkerd.io/inject: disabled, vault.hashicorp.com/agent-inject: "false") that only means anything on the pod a mesh webhook actually sees. A mover is a short-lived batch pod; an injected sidecar that never exits keeps its Job running forever. Same reserved keys as podLabels, rejected at admission.
podLabels
map[string]string
—
Extra labels for every mover POD (and the Job that owns it), merged UNDER kopiur's own labels — a key kopiur sets always wins, so a user-supplied value can never break the selectors the controller counts and reaps by. This is the hook for cluster machinery that keys off pod labels and that kopiur has no field of its own for: a Kueue kueue.x-k8s.io/queue-name to put movers under a cluster queue, a monitoring/NetworkPolicy selector, a service-mesh exclusion label. Keys under kopiur.home-operations.com/ and the exact key app.kubernetes.io/managed-by are reserved and rejected at admission.
podSecurityContext
core/v1 PodSecurityContext
—
Pod security-context base (notably fsGroup) for every mover.
resources
core/v1 ResourceRequirements
—
Resource requests/limits base for the mover container.
COMPLIANCE — nobody can shorten or remove the lock before it expires, including the account root. An oversized period is an unfixable storage-cost commitment; there is no recovery path short of deleting the bucket after expiry.
disabled
boolean
—
Actively disable retention (--retention-mode=none). Must be true. A bool rather than a unit variant because an externally-tagged unit variant serializes as the bare string "Disabled", mixing string and object forms in one oneOf and breaking the structural schema. Same shape, and same reason, as crate::cluster_repository::AllowedNamespaces::All. This is distinct from omitting blobRetention entirely: absent means "leave the repository alone", so deleting the block from a manifest can never silently strip ransomware protection someone configured deliberately.
GOVERNANCE — locked against ordinary deletes, but a sufficiently privileged identity can still shorten or remove the lock. The safe default for most clusters.
Go-style duration; kopia's minimum is 24h and there is no maximum. Accepts h/m/s (or a bare number of seconds) — not d. Note that kopia's own CLI does take 30d, so a period copied from kopia's documentation is rejected here; write 30 days as 720h. Kopiur deliberately keeps one duration grammar across every CRD field rather than matching kopia's per-flag variations.
Go-style duration; kopia's minimum is 24h and there is no maximum. Accepts h/m/s (or a bare number of seconds) — not d. Note that kopia's own CLI does take 30d, so a period copied from kopia's documentation is rejected here; write 30 days as 720h. Kopiur deliberately keeps one duration grammar across every CRD field rather than matching kopia's per-flag variations.
Index blobs in an epoch that trigger an advance, once older than minDuration (kopia default 20).
advanceOnSizeMiB
integer
—
Total index size in an epoch that triggers an advance, once older than minDuration (kopia default 10 MiB). Named MiB, not MB, with an explicit rename rather than the derived camelCase (advanceOnSizeMb, which reads as megabit). The unit is genuinely mebibytes — kopia's --epoch-advance-on-size-mb multiplies by 1048576, so 10 is 10485760 bytes — even though kopia's own log renders the result as "MB". That ambiguity is this field's main hazard; the API surface should not reproduce it.
checkpointFrequency
integer
—
Epochs between full index checkpoints (kopia default 7).
deleteParallelism
integer
—
Parallelism for epoch cleanup deletions (kopia default 4).
minDuration
string
—
Minimum epoch age before it may advance (kopia default 24h). A Go-style duration (6h, 90m). The advance gate — no blob count closes an epoch younger than this.
refreshFrequency
string
—
How often clients re-read epoch state (kopia default 20m). Go-style duration.
Deterministic jitter window (Go-style duration, e.g. 10m) applied to every consuming cron that doesn't set its own jitter — SnapshotSchedule, Maintenance (quick and full), SnapshotPolicy verification (quick and deep), and both replication kinds. Set once here instead of repeating it on every cron, so a whole repository's schedules spread their load off the top-of-the-hour thundering herd. The spread is deterministic per (scheduleUID, slot), not random: the same slot always lands on the same instant, so a restart never re-rolls it. Capped at 24h by validation — jitter is a spread WITHIN a cron period, not a schedule offset.
timezone
string
—
IANA timezone name applied to every consuming cron that doesn't set its own timezone (e.g. America/New_York). Set once here instead of repeating it on every SnapshotPolicy.verification, RepositoryReplication.schedule, and Maintenance.schedule cron.
Where the seed data comes from: exactly one of a bare storage backend (blob mode) or another repository CR (migrate mode).
allowEmptySource
boolean
—
Accept a source that holds zero snapshots (default false). A mirror that answers but is empty is almost always a mis-pointed bucket/prefix, and silently seeding nothing would hand you a Ready repository with no history — the failure mode #380 is about. By default the bootstrap fails loudly and retries; set this when an empty source is genuinely expected (e.g. re-homing a repository whose history was deliberately pruned to nothing).
Opt-in projection of the SOURCE repository's credential Secrets into the seeding mover Job's namespace. Migrate mode only in practice — a blob-mode source's credentials must already be in the namespace the bootstrap Job runs in (this CR's own namespace for a Repository; for a ClusterRepository the operator's namespace, unless encryption.passwordSecretRef.namespace pins another, in which case the Job runs there). Requires the operator's features.credentialProjection install flag.
Deadline/backoff for the seeding bootstrap Job. An absent activeDeadlineSeconds means 24h here, rather than the 120s a routine connect gets — a seed copies a whole repository, once. Only applied while the seed is armed; later connects to the now-initialized repository use spec.bootstrap.failurePolicy as before.
A bare storage backend holding a byte-for-byte mirror of a kopia repository (blob mode, kopia repository sync-to). The mirror's format and encryption password are inherited verbatim, so this repository's encryption.passwordSecretRef must already hold the MIRROR's password and spec.create's format knobs are refused as inert.
Another Repository/ClusterRepository, opened read-only (migrate mode, kopia snapshot migrate). Snapshot identities and times are preserved, so seeded history is restorable by identity/fromPolicy. A kind: Repository reference with no namespace resolves in the referrer's own namespace — and, on a cluster-scoped ClusterRepository (which has none), in the operator's namespace, the same rule its credential secretRefs follow. Set namespace explicitly whenever the source lives anywhere else.
Google Drive via kopia's native gdrive provider (service-account JSON). kopia marks this provider experimental / not maintained, and a native gdrive repository is not interchangeable with an rclone-backed Drive remote.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Secret holding the Google service-account JSON used to reach the folder, read by the well-known key KOPIA_GDRIVE_CREDENTIALS. Absent means kopia falls back to ambient credentials (GOOGLE_APPLICATION_CREDENTIALS or instance metadata).
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Secret holding the rclone.conf that defines the remote referenced by remote_path.
startupTimeout
string
—
How long kopia waits for its embedded rclone serve to come up before failing the connect, as a Go duration (e.g. 2m). kopia's default is 15s; raise it for slow remotes whose repository metadata/indexes load through the rclone/WebDAV bridge and take longer than the default budget.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Name of the ConfigMap holding the value (e.g. a CA bundle). Resolved in the referrer's namespace for a namespaced Repository, and in the operator's namespace (KOPIUR_NAMESPACE) for a ClusterRepository (cluster-scoped, no namespace of its own).
key
string
—
Which key inside the ConfigMap to read; defaults to ca.crt when unset.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Copy only each source identity's most recent snapshot instead of its full history (kopia snapshot migrate --latest). Default false — a seed exists to recover history, so the full copy is the sane default; set this when you only need the latest restore point back quickly.
parallel
integer
— min 0
--parallel: snapshots migrated concurrently (kopia default 1 — sequential). Must be >= 1 when set.
policies
enum: none | copy | copyOverwrite
none
Whether the source's kopia policies are copied along with the snapshots. Defaults to PolicyCopyMode::None (an explicit --no-policies), not kopia's own copy-by-default: retention in a kopiur-managed repository is driven by Snapshot CRs, and importing the source's kopia-side policies could delete manifests behind the operator's back.
Bandwidth/ops caps for THIS seed's copy, per side. A migrate seed opens two repositories under two kopia connections and snapshot migrate has no speed flags of its own, so each side is applied as kopia repository throttle set on that side's connection: source caps the REPLICA — the repository named by spec.seed.from.repository, opened read-only — overriding itsmoverDefaults.throttle; * destination caps THIS repository, the one being seeded, overriding its own* moverDefaults.throttle. Each side overrides field by field: a knob set here wins, a knob left unset keeps that repository's default. Absent: both sides use their repository's defaults. Applies only while the seed is armed — an ordinary connect to the now-initialized repository is capped by moverDefaults.throttle alone.
Caps for the DESTINATION (write) side, overriding the destination repository's moverDefaults.throttle field by field. Applied with repository throttle set on the destination connection the migrate writes through.
Caps for the SOURCE (read) side, overriding the source repository's moverDefaults.throttle field by field. Applied with repository throttle set on the migrate's read-only source connection — accepted there, so a read-only source is throttled like any other.
--max-download-speed: cap read throughput from the seed source, in bytes/sec (kopia default: unlimited).
maxUploadSpeedBytesPerSecond
integer
—
--max-upload-speed: cap write throughput into this repository, in bytes/sec (kopia default: unlimited).
parallel
integer
— min 0
--parallel: concurrent blob-copy workers (kopia default 1 — sequential). Raise it: a first-time seed of a large repository over a WAN is exactly the workload sequential copying is worst at.
Repository size and snapshot counts from the last catalog scan.
uniqueId
string
—
Kopia repository unique ID, pinned on the first successful bootstrap. Its presence is the "this repository has been Ready" flag that makes auto-create one-way in time: spec.create.enabled governs the FIRST bootstrap only, and once this is set kopiur will never create a fresh empty repository over the backend, however empty the backend goes. A wiped backend therefore parks at Failed with reason RepositoryReinitializeBlocked instead of being silently re-created. To deliberately re-initialize a wiped repository, annotate it with kopiur.home-operations.com/allow-reinitialize set to THIS value; the ack is honored only while it matches, so the new ID minted by a successful re-initialize makes it inert. This discards the history the old repository held.
How many Snapshot CRs were materialized from the catalog scan.
foreignSnapshotCount
integer
—
Snapshots in the last complete listing classified as another cluster's (see catalog.foreignSnapshots); never materialized under Ignore. As of catalog.lastRefreshAt — enable periodicRefresh to keep it current.
lastRefreshAt
string
—
RFC 3339 timestamp of the last catalog refresh.
scanRequestAttemptAt
string
—
RFC 3339 timestamp of the last bootstrap/scan attempt initiated BECAUSE OF a pending catalog-scan-requested-at token (i.e. the token arm was the reason the attempt fired). Used only to rate-limit token-driven attempts on a Ready-but-unreachable repository — it is never compared against the token for retirement (that's scanRequestHonored, by equality).
scanRequestHonored
string
—
The RFC3339 token VALUE of the kopiur.home-operations.com/catalog-scan-requested-at annotation last honored by a completed catalog scan. Compared by equality against the live annotation to decide whether a requested on-demand scan is still pending — deliberately NOT a timestamp comparison against lastRefreshAt (a periodic refresh completing after the request was made would otherwise look like it honored the request even though it started before the annotation was set).
lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
message
string
required
message is a human readable message indicating details about the transition. This may be an empty string.
reason
string
required
reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
status
string
required
status of the condition, one of True, False, Unknown.
type
string
required
type of condition in CamelCase or in foo.example.com/CamelCase.
observedGeneration
integer
—
observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditionsx.observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
Consecutive failing probes accrued; reset to zero on any success. The loud condition is raised only once this reaches the failure threshold.
firstFailureAt
string
—
RFC 3339 timestamp of the first failure in the current failing streak.
lastHealthyAt
string
—
RFC 3339 timestamp of the last successful probe (backend reachable, repo present).
lastProbeAt
string
—
RFC 3339 timestamp of the last completed probe (success or failure); drives the health_probe_due timer so the probe re-fires on cadence.
probeAttemptAt
string
—
RFC 3339 timestamp at which the last backend health probe was launched (the bootstrap Job created for it), cleared when its result is finalized. This is the launch-side rate limit, and it is what makes the probe terminate. lastProbeAt is written only at finalize, so gating the launch on that alone recycles the bootstrap Job forever: the gate destroys every Job whose completion would have cleared it (#273). Never compared against lastProbeAt — see kopiur_controller::health::probe_action.
Which copy mechanism a seed ran. Mirrors the SeedSource variant, named after the operation rather than the input so status, metrics (kopiur_repository_seed_total{mode}) and docs share one vocabulary.
seededAt
string
—
RFC 3339 timestamp the seed completed. Set once: a repository is seeded exactly once, at its first bootstrap.
snapshotCount
integer
—
Snapshots observed at the SOURCE when the seed ran. Zero is only ever recorded when allowEmptySource permitted it.
snapshotsCopied
integer
—
Snapshots actually copied into this repository. Migrate mode only — a blob copy moves storage, not manifests, so there is no per-snapshot copy count to report and it leaves this unset; its snapshotCount is the listing taken at the SOURCE before the copy, which for a byte-for-byte mirror is also what this repository ends up holding.
source
string
—
The source the data came from, rendered by SeedSource::describe — the backend discriminant (S3, Filesystem, …) for blob mode, Kind/name for migrate mode. Never a credential or a bucket path.
startedAt
string
—
RFC 3339 timestamp the operator LAUNCHED a seeding bootstrap Job for this repository — the durable seed-attempt marker. Stamped before the Job is created, and never cleared. Its whole job is to distinguish "a seed this operator started did not finish" from "this backend was initialized by somebody else": the first must resume the copy, the second must keep the no-clobber AlreadyInitialized path. See seed_resume — the marker is the ONLY input the resume decision is allowed to take, because a resuming migrate writes into whatever repository is at the backend and then re-stamps its maintenance owner.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Number of content-index blobs (kopia index list) observed at the last bootstrap.
lastObservedAt
string
—
RFC 3339 timestamp these stats were last observed.
snapshotCount
integer
—
Total snapshots present in the repository (across all identities).
totalSize
string
—
Human-readable total on-disk size (e.g. 412Gi).
totalSizeBytes
integer
—
Logical bytes under management (the integer form of total_size): the sum, over each distinct snapshot source, of the most-recent snapshot's logical size. Exposed to backup preflight as repository.sizeBytes. This is repository total size, not backend free space (object stores don't report remaining capacity).
Externally tagged — set exactly one of: repositories · repository.
Field
Type
Default
Description
adoption
enum: Adopt | Ignore
—
Whether a discovered snapshot whose resolved identity matches a live SnapshotPolicy is automatically adopted — re-attached (stamped with that policy's config label, status.origin flipped to Adopted) so GFS retention governs it and eventually prunes it, instead of it sitting in the catalog forever as an immortal discovered row.
Opt-in credential-Secret projection into each backup mover's namespace (default off).
defaultDeletionPolicy
enum: Delete | Retain | Orphan
Delete
Lifecycle of the underlying kopia snapshot when its Snapshot CR is deleted. Produced backups default to Delete; discovered snapshots are forced to Retain.
Paths/patterns kopia should skip while snapshotting.
groupBy
enum: VolumeGroupSnapshot | None
VolumeGroupSnapshot
Multi-PVC grouping strategy. Defaults to a consistent group snapshot across all PVCs; set Noneexplicitly to accept independent per-PVC snapshots, because a silent per-PVC fallback would produce inconsistent backups.
Named CEL preconditions evaluated before each backup run; opt-in (absent ⇒ no preflight). A failing check holds the Snapshot in Pending (PreflightFailed) and, after timeout, fails it.
Multi-repository fan-out: every listed Repository/ClusterRepository receives its own independent backup of each source — one Snapshot CR + one mover Job per (source, repository) pair, so the captures are separate kopia snapshots, not copies of one another. Identity resolves per-repo under that repository's identityDefaults. Mutually exclusive with repository (exactly one of the two is set) AND with hooks: with N concurrent children the first finisher would run the thaw hook while the other N-1 movers still read — use a single-repo policy plus a SnapshotReplication when hooks are needed for a second target.
First-class backup verification; opt-in (absent ⇒ no verification runs).
volumeSnapshotClassName
string
—
VolumeSnapshotClass used when copyMethod snapshots/clones the source. Absent or empty both mean auto-select the default class for the source PVC's CSI driver, so a GitOps-templated value (Flux/Kustomize ${VAR}) is safe when the variable is unset.
Abort the snapshot at the first error instead of collecting and continuing (snapshot create --fail-fast; kopia default: false). This is a snapshot create argv flag, not a policy set knob, but lives beside its semantic opposites (ignore*Errors) for discoverability.
ignoreDirErrors
boolean
—
Continue the snapshot when a directory cannot be read (--ignore-dir-errors).
ignoreFileErrors
boolean
—
Continue the snapshot when a file cannot be read (--ignore-file-errors).
ignoreUnknownTypes
boolean
—
Continue past entries of unknown type (--ignore-unknown-types).
Filename/path globs to exclude from the snapshot (e.g. *.tmp, */cache/*). Absent ⇒ default_ignore_rules (OS-artifact junk: /lost+found, System Volume Information, $RECYCLE.BIN, @eaDir, .snapshot). An explicit list REPLACES the default wholesale (re-add any entries you still want); explicit ignoreRules: [] opts fully out. NOT skip_serializing_if — an explicit empty list must round-trip as [], not vanish back to "absent" (which would silently resurrect the default on the next parse).
Copy the UID/GID security context from a live workload rather than hard-coding it. Requires the workload to pin runAsUser (container or pod level): a UID that comes from the container image's USER line is invisible in the pod spec and cannot be inherited — the mover would silently run as its own image's UID instead. May be combined with securityContext/podSecurityContext, which override it field-wise and act as the fallback when no workload pod can be resolved.
podSecurityContext
core/v1 PodSecurityContext
—
Pod security context for the mover (notably fsGroup for group-writable restore volumes). Same layering as securityContext: highest layer, merged field-wise, and combinable with inheritSecurityContextFrom.
privilegedMode
boolean
—
Opt-in, namespace-gated privileged mode; preserves UID/GID on restore.
resources
core/v1 ResourceRequirements
—
Resource requests/limits for the mover container.
securityContext
core/v1 SecurityContext
—
Container security context for the mover; merged field-wise over the hardened base, moverDefaults, and any inherited context — this is the highest layer, so every field set here wins. Combines with inheritSecurityContextFrom: fields you set override the workload's, fields you omit are inherited, and this context stands in alone when inheritance cannot resolve a pod.
ttlSecondsAfterFinished
integer
—
Per-recipe override of Job.spec.ttlSecondsAfterFinished so finished Jobs self-GC.
Backup sources only: auto-derive the workload from the PVC this snapshot backs up.
snapshot
object
—
Restores only: inherit the identity RECORDED on the backup itself (Snapshot.status.recorded, decoded from the kopiur-meta kopia tag) — uid/gid/fsGroup the backup mover actually ran as. Needs no live workload pod, so it works on a rebuilt cluster and with target.populator. Rejected at admission on SnapshotPolicy/Maintenance (backups read the live workload; maintenance has no snapshot). Write it as snapshot: {} (an empty sub-object) — a bare snapshot: is null and rejected.
Checks that must all pass (AND) before the backup launches. An empty list is an inert no-op (symmetric with verification absent).
timeout
string
—
How long to hold a Snapshot in Pending while a check is unsatisfied before failing it (Go-style duration like 10m or 1h; default 10m). A zero duration (0/0s) holds indefinitely (never fail on preflight).
Externally tagged — set exactly one of: nfs · pvc · pvcSelector.
Field
Type
Default
Description
acknowledgeLiveMutation
boolean
—
Acknowledges that copyMethod: Direct + readOnly: false lets the kubelet recursively chgrp the live volume to the mover's fsGroup and make it group-writable — permanently, while the workload is running. Required for that combination alone. Ignored (not rejected) otherwise: it is an acknowledgement, never harmful to carry, and rejecting a stale one would make switching copyMethod between Direct and Snapshot/Clone a two-step edit in both directions.
Label/namespace selector matching many PVCs. Mutually exclusive with pvc/nfs.
readOnly
boolean
true
Mount the source read-only (default true; kopia only ever reads it). Set falseonly to make fsGroup work on the source. The kubelet applies fsGroup by recursively chgrp-ing the volume and adding group-write — and it skips that walk entirely on a read-only mount, which is why a mover fsGroup/fsGroupChangePolicy otherwise has no effect here. Under copyMethod: Snapshot/Clone the walk rewrites the throwaway staged PVC and never touches your data. Under copyMethod: Direct it rewrites the LIVE volume, which requires acknowledgeLiveMutation. Not supported on an nfs source: the kubelet does not apply fsGroup to in-tree NFS volumes at all, so a read-write mount would grant nothing.
sourcePathOverride
string
— maxLength 4096
What kopia records as the source path (default /pvc/<name>, or the NFS export path).
sourcePathStrategy
enum: PvcName | PvcNamespacedName
PvcName
How a selector-matched PVC's source path is derived. Only relevant for pvcSelector sources, where one recipe expands to many PVCs and each needs a distinct kopia source path. Defaults to PvcName.
Access modes for the staged PVC. Empty ⇒ copy the source PVC's modes. ReadOnlyMany pairs with snapshot-backed read-only classes (e.g. CephFS backingSnapshot); the mover mounts the staged PVC read-only to match, and rejects it at admission if a source sets readOnly: false (a read-only stage cannot be mounted read-write).
storageClassName
string
—
StorageClass for the staged PVC — the temporary PVC restored from the CSI VolumeSnapshot (copyMethod: Snapshot) or cloned from the source (copyMethod: Clone). Absent ⇒ the staged PVC copies the source PVC's class. Must belong to the same CSI driver as the source (staging fails fast on a mismatch). Flagship use: a rook-ceph CephFS class with backingSnapshot: "true", which mounts the snapshot shallowly (metadata-only, near-instant, read-only) instead of running a full subvolume clone that can take many minutes on small-file-heavy volumes.
timeout
string
—
How long each staging phase may take before the backup is failed (Go-style duration like 10m or 1h; default 10m): first the staged VolumeSnapshot becoming readyToUse (measured from its creation), then — on an Immediate-binding StorageClass — the staged PVC binding (a fresh budget measured from the PVC's creation, covering the CSI restore/clone). A transient CSI/snapshot-controller error during either wait is retried, never fatal on its own — only this deadline fails staging. A zero duration (0/0s) waits indefinitely. Raise this for backends whose snapshots or clones take long (e.g. cloud snapshots of large volumes, CephFS full clones of small-file-heavy volumes).
snapshot create --upload-limit-mb: abort the snapshot once this many MB have been uploaded (kopia default: 0 — unlimited). Named limitMb rather than uploadLimitMb to avoid the upload.uploadLimitMb stutter; like failFast, this is a snapshot create argv flag, not a policy set knob, but lives here beside its parallelism siblings.
maxParallelFileReads
integer
—
--max-parallel-file-reads: file-read concurrency within a snapshot.
maxParallelSnapshots
integer
—
--max-parallel-snapshots: how many sources snapshot concurrently.
Quick (blob-level) verification tier; absent ⇒ no quick verification. Its cron lives under quick.schedule (matching deep.schedule), see QuickVerification.
successExpr
string
—
CEL pass/fail predicate over the verify result; applies to both tiers.
verifyFilesPercent
integer
— min 0; max 255
How many files quick verifies fully (--verify-files-percent); absent leaves kopia's default.
Standard Kubernetes conditions (e.g. RepositoryReachable, GroupSnapshotSupported).
lastSuccessfulSnapshot
string
—
RFC3339 timestamp of the most recent successful child Snapshot from this recipe.
lastVerified
string
—
RFC3339 timestamp of the most recent successful verification (any tier). Single-repo: stamped directly by the verify mover. Multi-repo: computed by the controller as the MINIMUM lastVerified across the CURRENT repositories ("everything is verified as of T"), absent until every current repository has verified at least once.
observedGeneration
integer
—
metadata.generation last reconciled, for staleness detection.
repositorySummary
string
—
Human-readable summary of the policy's repository target(s) for the Repositories print column: the comma-joined repository names (the one name for the single-repo shape), capped near a kubectl column width with a +N overflow marker. Written by the controller.
Per-repository verification records for a multi-repository policy (#368): one entry per CURRENT spec.repositories member, maintained by the controller (single writer — entries for repositories no longer in the spec are pruned). Empty (elided) for the single-repo shape, whose wire stays byte-identical.
verificationStamps
map[string]string
—
Internal write channel for per-repository verification (#368): RFC3339 markers keyed by the normalized repository key (repo_key). Each verify mover merge-patches ONLY its own key — a JSON merge patch merges map keys, so two concurrent per-repo verifies can never clobber one another (a Vec would be replaced wholesale). The controller folds these into verification on its next pass and prunes keys for repositories no longer in the spec. Never written for the single-repo shape.
Number of discovered Snapshot CRs adopted by the last adoption pass.
lastAdoptionAt
string
—
RFC3339 timestamp of the last adoption pass that adopted at least one snapshot.
lastScanMatched
integer
— min 0
Discovered snapshots whose kopia identity matched this policy at the most recent adoption pass, counted BEFORE the own-id and retention filters — so it reads "history relevant to me exists in the catalog", independent of whether that pass adopted anything. A multi-repository policy SUMS this across the ready repositories of the pass. 0 together with lastScanUnmatched: 0 means the catalog was empty at that pass; absent means no adoption pass has run yet. Neutral inventory, not a verdict: 0 matched beside a non-zero lastScanUnmatched is the ordinary shape of a new policy on a shared repository, and is also what a post-disaster-recovery identity mismatch looks like — compare status.resolved.identity with the pre-disaster configuration to tell them apart.
lastScanUnmatched
integer
— min 0
Discovered snapshots the most recent adoption pass saw that did NOT match this policy's identity (other policies' or other clusters' history in a shared repository). Same counting rules as lastScanMatched: pre-filter, summed across a multi-repository policy's ready repositories, 0/0 for an empty catalog, absent when no pass has run.
scanRequestedAt
string
—
RFC3339 token echoing an in-flight on-demand adoption scan request for this policy's identity; cleared once honored.
scanRequestedIdentity
string
—
The resolved kopia identity the requested scan was scoped to, pinned at request time so a later identity-changing edit can't retarget an in-flight scan.
skippedByRetention
integer
— min 0
Identity-matching discovered snapshots the last adoption pass left discovered because spec.retention would prune them immediately under the effective deletionPolicy (Retain/Orphan — a CR-only prune that would re-discover and re-adopt forever). 0/absent when nothing was withheld. See the AdoptionSkippedByRetention event for the levers.
totalAdopted
integer
— min 0
Running total of Snapshot CRs ever adopted into this recipe.
lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
message
string
required
message is a human readable message indicating details about the transition. This may be an empty string.
reason
string
required
reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
status
string
required
status of the condition, one of True, False, Unknown.
type
string
required
type of condition in CamelCase or in foo.example.com/CamelCase.
observedGeneration
integer
—
observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditionsx.observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
Per-repository resolution for a MULTI-repository policy (spec.repositories): one entry per member, each carrying the identity resolved under THAT repository's identityDefaults (the unit of identity is the (repository, identity) pair — N members means N independent kopia lineages). Empty — and elided from the wire — for the classic single-repo shape, whose resolution stays in the top-level identity/sources fields exactly as before this field existed.
The username@hostname identity resolved under this repository's identityDefaults; absent when it could not be resolved (the guard treats an absent baseline as "no baseline" and degrades to allow for that member only).
Lifecycle of the underlying kopia snapshot when its Snapshot CR is deleted. Produced backups default to Delete; discovered snapshots are forced to Retain.
description
string
— maxLength 1024
Free-form text recorded on the kopia snapshot manifest (snapshot create --description). Per-invocation by nature — scheduled/discovered Snapshots never set this (no templated descriptions).
What the deletion of a SnapshotSchedule does to the Snapshot CRs it produced (which Kubernetes GC cascade-deletes via their ownerReference). Default Retain: the CRs are removed but their kopia snapshots survive and the catalog rediscovers them as origin: discovered. Delete opts into the cascade: each Snapshot's own deletionPolicy applies. Deliberately 2-variant (not reusing DeletionPolicy): an Orphan in cascade position would differ from Retain only in per-CR event/metric bookkeeping — an invalid state made unrepresentable. The guard's Retain is exactly DeletionPolicy::Retain's semantics (CR removed, kopia snapshot stays, catalog rediscovers it), deliberately NOT the Orphan event storm (no per-CR "orphaned" event/metric for every produced Snapshot).
The ONE repository this Snapshot targets, pinned by value at mint time. Stamped by a multi-repository SnapshotPolicy fan-out (each child covers exactly one member of the policy's repository set) and by SnapshotReplication copy CRs (the destination repository). Absent for the legacy single-repository case, where the policy's own spec.repository (or, for catalog rows, the owning repository CR) is the answer — an absent pin resolves exactly as before this field existed, so pre-feature Snapshots are untouched.
The ONE concrete source this Snapshot covers, when policyRef names a recipe whose sources[] expands to many — i.e. a pvcSelector. Stamped by whoever minted the CR: a SnapshotSchedule fire, or kubectl kopiur snapshot now. Absent for the ordinary single-source case, where the policy's own sources0 is the target. Absent against a selector policy is refused rather than guessed. The operator must never pick a PVC on the user's behalf: silently backing up one arbitrary volume out of N looks exactly like success.
tags
map[string]string
—
Free-form tags attached to the kopia snapshot manifest itself (snapshot create --tags), e.g. reason: pre-upgrade — durable in the repository, independent of this CR. Keys must be non-empty, colon-free (kopia splits on the first colon), and must not start with the reserved kopiur prefix; at most 10 tags, keys ≤ 63 bytes, values ≤ 256 bytes (webhook-enforced).
Zero-based index into policyRef's spec.sources this child expanded from. Pins WHICH source's knobs (readOnly, sourcePathOverride, sourcePathStrategy, acknowledgeLiveMutation) govern this run, so a policy carrying several sources stays unambiguous. An index that is out of range at reconcile time — the policy shrank mid-run — is a named terminal failure, never a silent fallback to sources0.
The consistency group this child belongs to, present only when the policy asked for one (groupBy: VolumeGroupSnapshot) AND the expansion produced more than one member in this namespace.
Namespace of the matched PersistentVolumeClaim. Explicit rather than inferred from the Snapshot's own namespace: a pvcSelector under a ClusterRepository may match across namespaces.
Namespace the VolumeGroupSnapshot lives in. A VolumeGroupSnapshot is namespaced and its source.selector is namespace-local, so a selector spanning namespaces yields ONE GROUP PER NAMESPACE, not one group. The consistency guarantee is per-namespace and this field is where that shows.
Lineage for origin: replicated rows: the source repository, source manifest id, and startTime the copy was migrated from. Written by the SnapshotReplication mover in the same atomic PATCH as status.snapshot; absent on every other origin. See CopiedFrom for why this lives on the CR rather than as kopia tags.
How a Snapshot came to exist. Canonical value mirrored from the kopiur.home-operations.com/origin label. Origin drives the deletion-policy default: discovered backups are forced to Retain because the operator did not create those snapshots.
The observed kopia-side pin state: Some(true) if pinned, Some(false) if unpinned, None before any pin reconcile.
preflightSince
string
—
RFC 3339 timestamp of the first reconcile where the repository was Ready but a spec.preflight check was failing. The one-shot anchor for the preflight timeout deadline (so the budget covers preflight only, not the earlier repository-not-Ready wait). Cleared once every preflight check passes, so a later failing episode gets a fresh budget rather than a stale anchor.
The mover identity recorded on the kopia snapshot itself (the kopiur-meta tag): the resolved effective uid/gid/fsGroup the backup ran as, plus its provenance. Produced runs stamp this at launch (from the same value written into the tag); discovered rows decode it from the tag during the catalog scan. Absent for pre-feature snapshots, foreign backups without the tag, or a tag this operator version cannot decode.
When the run's projected credential Secrets were reclaimed (RFC 3339); absent until the reap has run. A projected copy is only needed while a mover Job can still load it via envFrom, but it is owner-ref'd to this CR, which long outlives that Job — so without an explicit reap it would sit in the workload namespace holding live repository credentials until the CR is pruned (#240).
lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
message
string
required
message is a human readable message indicating details about the transition. This may be an empty string.
reason
string
required
reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
status
string
required
status of the condition, one of True, False, Unknown.
type
string
required
type of condition in CamelCase or in foo.example.com/CamelCase.
observedGeneration
integer
—
observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditionsx.observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
The SOURCE repository the snapshot was migrated from (the SnapshotReplication's sourceRef, resolved at run time).
sourceManifestId
string
required
The kopia manifest id the snapshot had in the SOURCE repository. Migrate assigns a NEW manifest id on the destination (status.snapshot.kopiaSnapshotID); this is the old one, kept for cross-repository correlation.
startTime
string
required
The snapshot's RFC3339 startTime — preserved verbatim by migrate and the key (together with the identity triple) both idempotent re-migration and pruning: mirrorSource correlate source and destination rows on.
kopia error class (e.g. RepositoryUnavailable, AuthFailure).
message
string
required
A short human-readable message: what failed, why, and how to fix it.
retryRecommended
boolean
required
Whether retrying the same operation unchanged could succeed.
exitCode
integer
—
The process exit code, if one was reported.
op
string
—
The mover operation that failed, as a stable label (e.g. repository connect, snapshot create) — the values of the mover's KopiaOp::as_str(). Distinguishes a repository-level connect failure from a source-level failure (a broken PVC), which share kopiaErrorClass values (e.g. NotFound). Absent on failures that occurred outside a kopia invocation.
stderrTail
string
—
The last lines of kopia's stderr, if any were captured (bounded by MAX_LOG_TAIL_BYTES).
The kopiur-meta schema version this value was written under. Writers emit the lowest schema that represents the data (currently KOPIUR_META_SCHEMA_V1); readers reject only a schema NEWER than they understand (degrading to recorded-absent, never an error).
src
enum: inherited | explicit | defaults | unknown
required
Which layer pinned the recorded identity. Only inherited means the identity tracked the workload.
fsGroup
integer
—
The resolved pod-level fsGroup the mover ran with (the hardened default is 65532). Absent = no layer set one.
gid
integer
—
The resolved effective runAsGroup the mover ran as at backup time (container runAsGroup, else pod). Absent = image-determined.
uid
integer
—
The resolved effective runAsUser the mover ran as at backup time (container runAsUser, else pod). Absent = no layer pinned a UID, so the mover's UID was image-determined.
The recipe's spec.credentialProjection as it stood for this run. The deletion path re-projects the mover's credentials, but the opt-in lives on the SnapshotPolicy — which a user may delete first. Pinning it here lets the finalizer honor the opt-in that was actually in force, instead of reading an absent recipe as "projection off" and blocking on a Secret that was never meant to be namespace-local (#255). Absent only on a Snapshot that predates the pin or never ran; a run always writes it, including enabled: false, so absent stays distinguishable from off.
The username@hostname:path identity recorded for this snapshot.
kopiaSnapshotID
string
required
kopia's snapshot ID — the handle the finalizer uses to delete content.
description
string
—
The kopia snapshot description (snapshot create --description), when one is recorded and non-empty. For discovered rows this is copied from the repository listing TRUNCATED to 1024 bytes (char-boundary-safe) — the value is foreign-writer-controlled and must never fail the CR write.
The resolved capture method (Snapshot or Clone) that produced this stage.
pvcName
string
—
Name of the staged PersistentVolumeClaim the mover mounts in place of the live source PVC.
ready
boolean
—
true once the stage is ready for the mover.
stagingTimeoutSeconds
integer
—
The resolved spec.staging.timeout (seconds) pinned when the stage was stamped, so the running-Job staged-PVC bind watchdog never re-resolves a policy that may have been edited or deleted mid-run. 0 = wait indefinitely.
storageClassName
string
—
StorageClass of the staged PVC — spec.staging.storageClassName when set, else the source PVC's class. Pinned for observability (e.g. confirming a CephFS shallow-clone class actually took effect).
volumeGroupSnapshotName
string
—
Name of the shared CSI VolumeGroupSnapshot this member staged from, when the recipe asked for a consistency group (groupBy: VolumeGroupSnapshot). Recorded because the group is otherwise invisible: it deliberately carries no ownerReferences (see io::group_staging), so this is how an operator tells which capture a backup came from — and how kubectl kopiur doctor finds one that outlived its members.
volumeSnapshotName
string
—
Name of the VolumeSnapshot created from the source PVC (copyMethod: Snapshot only).
Stamped onto every produced Snapshot at creation (spec.onScheduleDelete) and consulted by the Snapshot finalizer when the owning schedule is gone or replaced. Absent resolves to Retain.
lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
message
string
required
message is a human readable message indicating details about the transition. This may be an empty string.
reason
string
required
reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
status
string
required
status of the condition, one of True, False, Unknown.
type
string
required
type of condition in CamelCase or in foo.example.com/CamelCase.
observedGeneration
integer
—
observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditionsx.observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
The RFC3339 instant this slot fired (or is scheduled to); also accepts the scheduledAt alias.
jitter
string
—
The deterministic jitter window (Go-style duration, e.g. 10m) the cron was spread by when this slot was pinned. This struct is shared by nextSchedule, lastSchedule and lastSuccessfulSchedule, so the field appears on all three, but the controller only ever WRITES it on nextSchedule — it is a property of a pin the controller may still have to invalidate, not a record of a slot that already fired. Recorded for the same reason as the pinned timezone: the window may be INHERITED from the target repository's scheduleDefaults.jitter, so a change to that default (or to spec.schedule.jitter) must invalidate the pinned wall-clock slot and recompute it in the new window — otherwise the edit would only take effect an arbitrary slot later. Absent both when no jitter applies and on legacy pins written before this field existed; an absent recorded window is treated as "unchanged" so an upgrade never churns an established pin.
The Snapshot CR this slot produced, when one was created.
timezone
string
—
The IANA timezone the cron was evaluated in when this slot was pinned. This struct is shared by nextSchedule, lastSchedule and lastSuccessfulSchedule, so the field appears on all three, but the controller only ever WRITES it on nextSchedule — it is a property of a pin the controller may still have to invalidate, not a record of a slot that already fired. Recorded so the controller can detect an effective-timezone change — a spec.schedule.timezone edit or a change to the target repository's scheduleDefaults.timezone — and invalidate the pinned wall-clock slot, recomputing it in the new zone. Absent on legacy pins written before this field existed (treated as "unchanged").
The RFC3339 instant this slot fired (or is scheduled to); also accepts the scheduledAt alias.
jitter
string
—
The deterministic jitter window (Go-style duration, e.g. 10m) the cron was spread by when this slot was pinned. This struct is shared by nextSchedule, lastSchedule and lastSuccessfulSchedule, so the field appears on all three, but the controller only ever WRITES it on nextSchedule — it is a property of a pin the controller may still have to invalidate, not a record of a slot that already fired. Recorded for the same reason as the pinned timezone: the window may be INHERITED from the target repository's scheduleDefaults.jitter, so a change to that default (or to spec.schedule.jitter) must invalidate the pinned wall-clock slot and recompute it in the new window — otherwise the edit would only take effect an arbitrary slot later. Absent both when no jitter applies and on legacy pins written before this field existed; an absent recorded window is treated as "unchanged" so an upgrade never churns an established pin.
The Snapshot CR this slot produced, when one was created.
timezone
string
—
The IANA timezone the cron was evaluated in when this slot was pinned. This struct is shared by nextSchedule, lastSchedule and lastSuccessfulSchedule, so the field appears on all three, but the controller only ever WRITES it on nextSchedule — it is a property of a pin the controller may still have to invalidate, not a record of a slot that already fired. Recorded so the controller can detect an effective-timezone change — a spec.schedule.timezone edit or a change to the target repository's scheduleDefaults.timezone — and invalidate the pinned wall-clock slot, recomputing it in the new zone. Absent on legacy pins written before this field existed (treated as "unchanged").
The RFC3339 instant this slot fired (or is scheduled to); also accepts the scheduledAt alias.
jitter
string
—
The deterministic jitter window (Go-style duration, e.g. 10m) the cron was spread by when this slot was pinned. This struct is shared by nextSchedule, lastSchedule and lastSuccessfulSchedule, so the field appears on all three, but the controller only ever WRITES it on nextSchedule — it is a property of a pin the controller may still have to invalidate, not a record of a slot that already fired. Recorded for the same reason as the pinned timezone: the window may be INHERITED from the target repository's scheduleDefaults.jitter, so a change to that default (or to spec.schedule.jitter) must invalidate the pinned wall-clock slot and recompute it in the new window — otherwise the edit would only take effect an arbitrary slot later. Absent both when no jitter applies and on legacy pins written before this field existed; an absent recorded window is treated as "unchanged" so an upgrade never churns an established pin.
The Snapshot CR this slot produced, when one was created.
timezone
string
—
The IANA timezone the cron was evaluated in when this slot was pinned. This struct is shared by nextSchedule, lastSchedule and lastSuccessfulSchedule, so the field appears on all three, but the controller only ever WRITES it on nextSchedule — it is a property of a pin the controller may still have to invalidate, not a record of a slot that already fired. Recorded so the controller can detect an effective-timezone change — a spec.schedule.timezone edit or a change to the target repository's scheduleDefaults.timezone — and invalidate the pinned wall-clock slot, recomputing it in the new zone. Absent on legacy pins written before this field existed (treated as "unchanged").
Name of the SnapshotPolicy whose identity selects the snapshot.
asOf
string
—
Restore the newest snapshot at or before this RFC3339 timestamp (point-in-time).
namespace
string
—
Namespace of the SnapshotPolicy; absent = the Restore's own namespace.
offset
integer
0
Which snapshot to pick: 0 = latest, 1 = previous, and so on.
sourcePath
string
— maxLength 4096
The kopia source path to restore FROM, overriding the path kopiur derives from the policy. Normally the path is derived: a policy with one plain pvc:/nfs source contributes its own path, and a pvcSelector policy contributes the path its sourcePathStrategy would have produced for the PVC being restored (/pvc/<name> or /pvc/<namespace>/<name>) — the same rule the backup side used when it wrote the snapshot, so a fan-out restore fills each PVC from ITS OWN snapshot instead of the newest snapshot of any member. Set this when the derivation is ambiguous or wrong: selector sources that disagree on sourcePathStrategy/sourcePathOverride (kopiur fails closed rather than guess), a policy whose selector sources share one sourcePathOverride, or a cross-namespace target.pvcRef whose derived /pvc/<namespace>/<name> names a namespace the repository never saw. Mirrors IdentitySource::source_path: it selects which kopia source to READ, it does not change where the data is written.
Access modes for the new PVC; empty defaults to ReadWriteOnce. Closed enum in the schema; a non-canonical value persisted before enforcement decodes as PvcAccessMode::Unknown and is rejected per-CR with the value quoted (webhook + reconciler, via validate_access_modes) instead of poisoning the typed watcher.
capacity
string
—
Requested size of the new PVC (e.g. 100Gi).
storageClassName
string
—
StorageClass for the new PVC; absent uses the cluster default.
Copy the UID/GID security context from a live workload rather than hard-coding it. Requires the workload to pin runAsUser (container or pod level): a UID that comes from the container image's USER line is invisible in the pod spec and cannot be inherited — the mover would silently run as its own image's UID instead. May be combined with securityContext/podSecurityContext, which override it field-wise and act as the fallback when no workload pod can be resolved.
podSecurityContext
core/v1 PodSecurityContext
—
Pod security context for the mover (notably fsGroup for group-writable restore volumes). Same layering as securityContext: highest layer, merged field-wise, and combinable with inheritSecurityContextFrom.
privilegedMode
boolean
—
Opt-in, namespace-gated privileged mode; preserves UID/GID on restore.
resources
core/v1 ResourceRequirements
—
Resource requests/limits for the mover container.
securityContext
core/v1 SecurityContext
—
Container security context for the mover; merged field-wise over the hardened base, moverDefaults, and any inherited context — this is the highest layer, so every field set here wins. Combines with inheritSecurityContextFrom: fields you set override the workload's, fields you omit are inherited, and this context stands in alone when inheritance cannot resolve a pod.
ttlSecondsAfterFinished
integer
—
Per-recipe override of Job.spec.ttlSecondsAfterFinished so finished Jobs self-GC.
Backup sources only: auto-derive the workload from the PVC this snapshot backs up.
snapshot
object
—
Restores only: inherit the identity RECORDED on the backup itself (Snapshot.status.recorded, decoded from the kopiur-meta kopia tag) — uid/gid/fsGroup the backup mover actually ran as. Needs no live workload pod, so it works on a rebuilt cluster and with target.populator. Rejected at admission on SnapshotPolicy/Maintenance (backups read the live workload; maintenance has no snapshot). Write it as snapshot: {} (an empty sub-object) — a bare snapshot: is null and rejected.
Delete files in the target that are not present in the snapshot (exact mirror); off by default. Wired to kopia's --[no-]delete-extra (previously a silent no-op — see issue #216 gap sweep).
ignoreErrors
boolean
—
--[no-]ignore-errors: ignore all restore errors and continue (kopia default false).
ignorePermissionErrors
boolean
—
Continue past permission errors during restore (default true).
overwriteDirectories
boolean
—
--[no-]overwrite-directories: overwrite existing directories in the target (kopia default true).
overwriteFiles
boolean
—
--[no-]overwrite-files: overwrite existing files in the target (kopia default true).
overwriteSymlinks
boolean
—
--[no-]overwrite-symlinks: overwrite existing symlinks in the target (kopia default true).
What to do when the resolved source matches no snapshot. Defaults to Fail (fail-closed) so an explicit restore can never silently no-op; choose Continue to provision an empty volume instead (deploy-or-restore).
waitTimeout
string
—
How long to wait for the source snapshot to appear before giving up (e.g. 5m).
Per-claimant state for a target.populator restore, keyed by the claiming PVC's name (#443). A populator Restore is claimed by EVERY PVC whose spec.dataSourceRef names it, not just the first, and each claimant gets its own prime PVC, its own mover Job and its own kopia source path — so each needs its own state. A map, not a list, because an RFC-7386 merge patch merges map keys but REPLACES arrays: N concurrent populate movers each patch only their own key and can never clobber a sibling (the same reason as SnapshotPolicyStatus.verificationStamps). Absent for a direct target.pvc/target.pvcRef restore, which keeps using the top-level resolved/target/waitStartedAt/logTail/failure. The schema renders as an object with additionalProperties, which PRUNES unknown keys — so every field any writer (controller or mover) puts under claims.<pvc> must exist on RestoreClaimStatus.
When the policy.waitTimeout window OPENED (RFC3339) — the first reconcile on which the restore could actually proceed (its repository reached Ready, and for a target.populator a PVC already claims it), NOT when the Restore was created. The window does NOT open while the referenced Repository object or fromPolicySnapshotPolicy doesn't exist: the restore parks in Pending (ReferentAvailable=False, reason RestoreReferentMissing) and stays unstamped. It DOES still open for a snapshotRef whose Snapshot row doesn't exist yet (so onMissingSnapshot can fire for a ref that never appears) and for a restore whose mover Job already launched. Stamped once and then honored verbatim, so the window survives controller restarts and Job pod retries; cleared when a populator re-opens resolution for a re-created claim, so that claim gets the full window again. Absent means the window has not opened yet (or no policy.waitTimeout is configured, in which case there is no window to anchor).
lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
message
string
required
message is a human readable message indicating details about the transition. This may be an empty string.
reason
string
required
reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
status
string
required
status of the condition, one of True, False, Unknown.
type
string
required
type of condition in CamelCase or in foo.example.com/CamelCase.
observedGeneration
integer
—
observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditionsx.observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
kopia error class (e.g. RepositoryUnavailable, AuthFailure).
message
string
required
A short human-readable message: what failed, why, and how to fix it.
retryRecommended
boolean
required
Whether retrying the same operation unchanged could succeed.
exitCode
integer
—
The process exit code, if one was reported.
op
string
—
The mover operation that failed, as a stable label (e.g. repository connect, snapshot create) — the values of the mover's KopiaOp::as_str(). Distinguishes a repository-level connect failure from a source-level failure (a broken PVC), which share kopiaErrorClass values (e.g. NotFound). Absent on failures that occurred outside a kopia invocation.
stderrTail
string
—
The last lines of kopia's stderr, if any were captured (bounded by MAX_LOG_TAIL_BYTES).
Previous lease strings still recognized as SELF (a migration path): when kopia's currently-recorded maintenance owner matches the owner derived from one of these aliases, a run treats the lease as its own — it claims it and re-stamps owner, upgrading the recorded owner to the current format. The operator populates this when a repository's managed Maintenance moves to a cluster-qualified lease (identityDefaults.cluster), so the transition never yields the lease to what merely looks like a foreign owner.
takeoverPolicy
enum: Never | PromptCondition | Force
Never
What to do when the lease is already held by a different owner.
Copy the UID/GID security context from a live workload rather than hard-coding it. Requires the workload to pin runAsUser (container or pod level): a UID that comes from the container image's USER line is invisible in the pod spec and cannot be inherited — the mover would silently run as its own image's UID instead. May be combined with securityContext/podSecurityContext, which override it field-wise and act as the fallback when no workload pod can be resolved.
podSecurityContext
core/v1 PodSecurityContext
—
Pod security context for the mover (notably fsGroup for group-writable restore volumes). Same layering as securityContext: highest layer, merged field-wise, and combinable with inheritSecurityContextFrom.
privilegedMode
boolean
—
Opt-in, namespace-gated privileged mode; preserves UID/GID on restore.
resources
core/v1 ResourceRequirements
—
Resource requests/limits for the mover container.
securityContext
core/v1 SecurityContext
—
Container security context for the mover; merged field-wise over the hardened base, moverDefaults, and any inherited context — this is the highest layer, so every field set here wins. Combines with inheritSecurityContextFrom: fields you set override the workload's, fields you omit are inherited, and this context stands in alone when inheritance cannot resolve a pod.
ttlSecondsAfterFinished
integer
—
Per-recipe override of Job.spec.ttlSecondsAfterFinished so finished Jobs self-GC.
Backup sources only: auto-derive the workload from the PVC this snapshot backs up.
snapshot
object
—
Restores only: inherit the identity RECORDED on the backup itself (Snapshot.status.recorded, decoded from the kopiur-meta kopia tag) — uid/gid/fsGroup the backup mover actually ran as. Needs no live workload pod, so it works on a rebuilt cluster and with target.populator. Rejected at admission on SnapshotPolicy/Maintenance (backups read the live workload; maintenance has no snapshot). Write it as snapshot: {} (an empty sub-object) — a bare snapshot: is null and rejected.
lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
message
string
required
message is a human readable message indicating details about the transition. This may be an empty string.
reason
string
required
reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
status
string
required
status of the condition, one of True, False, Unknown.
type
string
required
type of condition in CamelCase or in foo.example.com/CamelCase.
observedGeneration
integer
—
observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditionsx.observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
The backend to mirror to; must differ from the source's backend (webhook-enforced). kopia repository sync-to is a blob-level copy: the destination inherits the source repository's format and encryption password verbatim, so there is no separate destination password to configure. The destination backend's own access credentials (e.g. S3 keys) ride its auth.secretRef, which — like the source's — must live in this CR's namespace.
Google Drive via kopia's native gdrive provider (service-account JSON). kopia marks this provider experimental / not maintained, and a native gdrive repository is not interchangeable with an rclone-backed Drive remote.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Secret holding the Google service-account JSON used to reach the folder, read by the well-known key KOPIA_GDRIVE_CREDENTIALS. Absent means kopia falls back to ambient credentials (GOOGLE_APPLICATION_CREDENTIALS or instance metadata).
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Secret holding the rclone.conf that defines the remote referenced by remote_path.
startupTimeout
string
—
How long kopia waits for its embedded rclone serve to come up before failing the connect, as a Go duration (e.g. 2m). kopia's default is 15s; raise it for slow remotes whose repository metadata/indexes load through the rclone/WebDAV bridge and take longer than the default budget.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Name of the ConfigMap holding the value (e.g. a CA bundle). Resolved in the referrer's namespace for a namespaced Repository, and in the operator's namespace (KOPIUR_NAMESPACE) for a ClusterRepository (cluster-scoped, no namespace of its own).
key
string
—
Which key inside the ConfigMap to read; defaults to ca.crt when unset.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Namespace of the Secret; absent = same namespace as the referrer. A ClusterRepository is cluster-scoped and has no namespace of its own, so when IT reads the Secret (to connect, to bootstrap, or to run its repository server) an absent namespace means the operator's namespace (KOPIUR_NAMESPACE). A workload mover (Snapshot/Restore/Maintenance) still needs the Secret in its OWN namespace — envFrom is namespace-local — so put it there, or use credentialProjection, which needs this namespace set EXPLICITLY to know what to copy. Set it whenever anything other than the operator itself reads the Secret.
Copy the UID/GID security context from a live workload rather than hard-coding it. Requires the workload to pin runAsUser (container or pod level): a UID that comes from the container image's USER line is invisible in the pod spec and cannot be inherited — the mover would silently run as its own image's UID instead. May be combined with securityContext/podSecurityContext, which override it field-wise and act as the fallback when no workload pod can be resolved.
podSecurityContext
core/v1 PodSecurityContext
—
Pod security context for the mover (notably fsGroup for group-writable restore volumes). Same layering as securityContext: highest layer, merged field-wise, and combinable with inheritSecurityContextFrom.
privilegedMode
boolean
—
Opt-in, namespace-gated privileged mode; preserves UID/GID on restore.
resources
core/v1 ResourceRequirements
—
Resource requests/limits for the mover container.
securityContext
core/v1 SecurityContext
—
Container security context for the mover; merged field-wise over the hardened base, moverDefaults, and any inherited context — this is the highest layer, so every field set here wins. Combines with inheritSecurityContextFrom: fields you set override the workload's, fields you omit are inherited, and this context stands in alone when inheritance cannot resolve a pod.
ttlSecondsAfterFinished
integer
—
Per-recipe override of Job.spec.ttlSecondsAfterFinished so finished Jobs self-GC.
Backup sources only: auto-derive the workload from the PVC this snapshot backs up.
snapshot
object
—
Restores only: inherit the identity RECORDED on the backup itself (Snapshot.status.recorded, decoded from the kopiur-meta kopia tag) — uid/gid/fsGroup the backup mover actually ran as. Needs no live workload pod, so it works on a rebuilt cluster and with target.populator. Rejected at admission on SnapshotPolicy/Maintenance (backups read the live workload; maintenance has no snapshot). Write it as snapshot: {} (an empty sub-object) — a bare snapshot: is null and rejected.
--delete: prune destination-only blobs so the mirror is an exact copy (kopia default false — additive sync, never removes destination content). Named deleteExtra, not kopia's bare delete: a delete: true key on backup-adjacent YAML is dangerously ambiguous at a glance. CAUTION: with this true, blobs present at the destination but absent from the source are deleted on every run.
maxDownloadSpeedBytesPerSecond
integer
—
--max-download-speed: cap read throughput from the source, in bytes/sec (kopia default: unlimited).
maxUploadSpeedBytesPerSecond
integer
—
--max-upload-speed: cap write throughput to the destination, in bytes/sec (kopia default: unlimited).
mustExist
boolean
—
--[no-]must-exist: fail the sync instead of initializing the destination's repository-format blob (kopia default false — sync-to may create the destination layout on first run).
parallel
integer
— min 0
--parallel: number of concurrent blob-copy workers (kopia default 1 — sequential, the root cause of #216's multi-week seed times to R2).
times
boolean
—
--[no-]times: synchronize blob modification times to the destination, when the destination backend supports it (kopia default true).
update
boolean
—
--[no-]update: update blobs already present at the destination when the source copy is newer (kopia default true).
lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
message
string
required
message is a human readable message indicating details about the transition. This may be an empty string.
reason
string
required
reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
status
string
required
status of the condition, one of True, False, Unknown.
type
string
required
type of condition in CamelCase or in foo.example.com/CamelCase.
observedGeneration
integer
—
observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditionsx.observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
The Repository or ClusterRepository snapshots are copied INTO. A real repository CR with its own password and format (unlike a RepositoryReplication destination, which is a bare backend reusing the source's password). Must not resolve to the same repository as sourceRef (webhook-enforced structurally; the shared validator catches the literal same-ref case at admission).
Opt-in projection of BOTH repositories' credential Secrets into the mover Job's namespace (required when the destination is a ClusterRepository whose Secrets live elsewhere).
What happens to already-replicated copies at the destination on later runs. Absent = none: copies accumulate forever and — like every mode here — survive deletion of this CR (copy Snapshot CRs carry no ownerReference to it). Pruning only ever considers snapshots this CR replicated (labeled at birth); it never touches the destination's own directly-written snapshots.
Bandwidth/ops caps for THIS replication's runs, per side. snapshot migrate has no speed flags, so each side is applied as kopia repository throttle set on that side's connection; source overrides the source repository's moverDefaults.throttle and destination the destination repository's, field by field (a field left unset keeps that repository's default). Absent: both sides use their repository's defaults.
Caps for the DESTINATION (write) side, overriding the destination repository's moverDefaults.throttle field by field. Applied with repository throttle set on the destination connection the migrate writes through.
Caps for the SOURCE (read) side, overriding the source repository's moverDefaults.throttle field by field. Applied with repository throttle set on the migrate's read-only source connection — accepted there, so a read-only source is throttled like any other.
Copy the UID/GID security context from a live workload rather than hard-coding it. Requires the workload to pin runAsUser (container or pod level): a UID that comes from the container image's USER line is invisible in the pod spec and cannot be inherited — the mover would silently run as its own image's UID instead. May be combined with securityContext/podSecurityContext, which override it field-wise and act as the fallback when no workload pod can be resolved.
podSecurityContext
core/v1 PodSecurityContext
—
Pod security context for the mover (notably fsGroup for group-writable restore volumes). Same layering as securityContext: highest layer, merged field-wise, and combinable with inheritSecurityContextFrom.
privilegedMode
boolean
—
Opt-in, namespace-gated privileged mode; preserves UID/GID on restore.
resources
core/v1 ResourceRequirements
—
Resource requests/limits for the mover container.
securityContext
core/v1 SecurityContext
—
Container security context for the mover; merged field-wise over the hardened base, moverDefaults, and any inherited context — this is the highest layer, so every field set here wins. Combines with inheritSecurityContextFrom: fields you set override the workload's, fields you omit are inherited, and this context stands in alone when inheritance cannot resolve a pod.
ttlSecondsAfterFinished
integer
—
Per-recipe override of Job.spec.ttlSecondsAfterFinished so finished Jobs self-GC.
Backup sources only: auto-derive the workload from the PVC this snapshot backs up.
snapshot
object
—
Restores only: inherit the identity RECORDED on the backup itself (Snapshot.status.recorded, decoded from the kopiur-meta kopia tag) — uid/gid/fsGroup the backup mover actually ran as. Needs no live workload pod, so it works on a rebuilt cluster and with target.populator. Rejected at admission on SnapshotPolicy/Maintenance (backups read the live workload; maintenance has no snapshot). Write it as snapshot: {} (an empty sub-object) — a bare snapshot: is null and rejected.
lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
message
string
required
message is a human readable message indicating details about the transition. This may be an empty string.
reason
string
required
reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
status
string
required
status of the condition, one of True, False, Unknown.
type
string
required
type of condition in CamelCase or in foo.example.com/CamelCase.
observedGeneration
integer
—
observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditionsx.observedGeneration is 9, the condition is out of date with respect to the current state of the instance.