Expand description
Cross-field validation the type system can’t express (ADR §2.2 principle 8).
These are the rules a single struct’s types can’t enforce: “field X is
forbidden only when sibling Y has a particular variant,” “this string must
parse as a cron,” “a discovered backup may only Retain.” They live here as pure
functions so the webhook calls them at admission and the controller calls them
defensively — one validator, two callers (SKILL hard-rule 4). No kube::Client,
no tokio.
§Fail-fast vs. accumulate (see crate::error)
Single-rule helpers return ValidationResult (fail-fast — first problem).
The per-CRD aggregate validators (validate_backup_config, …) return
Vec<ValidationError> so a user sees every independent problem in one apply.
An empty vec means valid.
Structs§
- Existing
Identity - An already-admitted
SnapshotPolicy’s identity, keyed for collision detection (ADR-0005 §6).repo_keyis a normalized repository identity (e.g."ClusterRepository/shared"or"Repository/backups/nas") so two policies are “the same repository” only when their keys match;nameis the policy’snamespace/namefor the actionable message.
Constants§
- CLUSTER_
NAME_ MAX_ LEN - Maximum length of
Repository/ClusterRepositoryidentityDefaults.cluster. A cluster identity is a short, human-chosen suffix appended onto a namespace name — not free text — so this is generous headroom well under DNS’s 253-byte label ceiling, not a real constraint in practice. - MAX_
SNAPSHOT_ TAGS - At most this many user tags per
Snapshot— unbounded user tags would inflate every kopia manifest AND the catalog result wire. - MAX_
SNAPSHOT_ TAG_ KEY_ LEN - Longest admissible user tag key, in bytes.
- MAX_
SNAPSHOT_ TAG_ VALUE_ LEN - Longest admissible user tag value, in bytes.
- NFS_
FSGROUP_ WARNING - The actionable admission warning for an inline-NFS filesystem repo whose
moverDefaultsgrant write access only viafsGroup.fsGroupis silently ignored on NFS (the kubelet doesn’t recursively chown in-tree NFS mounts), so the mover/server/bootstrap reach the export as the unprivileged uid and the repoconnect/createfails withpermission denied. Non-blocking (a user fixing it NAS-side via Mapall can ignore it). Kept short for the admission response (kube truncates very long warnings).
Functions§
- detect_
identity_ collision - Detect whether a
SnapshotPolicy’s resolved identity collides with an already-admitted policy’s identity in the same repository (ADR-0005 §6). Pure so the decision is unit-tested; the webhook does the IO (list policies, resolve each identity) and calls this. Returns the conflictingnamespace/nameorNone. - detect_
identity_ fork - Pure decision for the fork-on-edit guard on a
username@hostnamechange. ReturnsSome(IdentityWouldFork)iff the policy has snapshot history, the change was not acknowledged, and the resolved identity actually differs. The webhook does the IO (read the old object’s pinned identity + history, resolve the new identity) and calls this. - detect_
repository_ identity_ change - Pure decision for the repository
identityDefaults-edit guard. An edit to aRepository/ClusterRepository’sidentityDefaults(cluster,hostnameExpr, orusernameExpr) changes what every consumerSnapshotPolicyrelying on those defaults resolves to — silently, with no per-policy edit to acknowledge it (unlikedetect_identity_fork, which guards a policy’s own edit). ReturnsSome(ValidationError::RepositoryIdentityWouldFork)iffidentityDefaultsactually changed, at least one consumer has snapshot history, and the change is not acknowledged. The webhook does the IO (list consumerSnapshotPolicys, read the ack annotation) and calls this. - detect_
source_ path_ fork - Pure decision for the fork-on-edit guard on a per-source path change. A PVC’s kopia
source path is part of its identity, so changing
sourcePathOverrideon a PVC that already has history orphans that PVC’s snapshots exactly as a username/hostname change would. Sources are matched across the edit by PVC name (paths are never CEL-driven, so an old-vs-new spec diff is complete); selector/NFS sources are out of scope. Returns the first offending change. - replication_
destination_ differs - Whether a replication’s
destinationbackend differs from its source repository’s backend (ADR-0005 §13(d)). Replicating a repository to itself is a no-op (or a loop), so the webhook rejects it. Pure so the decision is unit-tested; the webhook resolves the source backend (it has a client) and calls this. A “same” destination is detected structurally by [backend_target_key]: same backend kind AND the same identifying target — which for S3 includes the endpoint and region (not just bucket+prefix), for Azure the storage account, and for a filesystem the backing volume, so two distinct providers that share a bucket/container/path name are NOT mistaken for the same repository (#248). - repository_
warnings - Non-blocking admission warnings for a
Repository/ClusterRepository. Shared by both handlers (the rules can’t fork). Today: the inline-NFS +fsGroup-only footgun (seeNFS_FSGROUP_WARNING). Takes the resolvedbackend+moverDefaultsso it serves both kinds without re-deriving them. - require_
min - A numeric knob must be at least
min— the shared one-liner behind everyOption<u32>count /Option<i64>bytes-per-second field (e.g.RepositoryReplication.spec.sync.parallel), so the rule and its message shape are written once instead of re-derived per field.fieldnames the exact path for the message (e.g."RepositoryReplication spec.sync.parallel"). Callers only invoke this for aSomevalue — an absent knob is always valid and never reaches this helper. - schedule_
cr_ growth_ warning - A non-blocking admission WARNING (never a rejection) when a schedule’s cron
fires more often than hourly (issue #249). Every fire creates one per-run
SnapshotCR per source, and they accumulate up to theSnapshotPolicyretention window — each terminal one is then re-reconciled for that whole window — so a sub-hourly cadence with a wide (or absent) retention can produce thousands of CRs. Sub-hourly is legitimate for some workloads, so this is a footgun heads-up, not a block. - snapshot_
tag_ error - Why one
spec.tagskey/value pair is invalid, orNonewhen it is clean. - validate_
access_ modes - Validate a PVC access-modes list wherever one appears (
spec.staging.accessModes,restore.target.pvc.accessModes). Three rules, one place, both callers (webhook at admission, controller defensively): - validate_
backend - Validate backend content the structural schema can’t express: the
inline-NFS volume on a
Filesystembackend, thesecretRefXORworkloadIdentityrule on the cloud-IAM backends, and Azure’s workload-identity prerequisites. Exhaustivematchso a newBackendvariant must be considered here before it compiles. - validate_
backend_ auth - A cloud-IAM backend’s
authblock is well-formed: exactly one ofsecretReforworkloadIdentitywhenauthis present (both areOptionbecause the forms share theauthkey, so it’s a webhook check — the same shape asvalidate_source). An absent/emptyauthis legal: the well-known keys may ride the encryption-password Secret, and an empty block means exactly that. A workload-identityserviceAccountNamemust be a valid object name, or the mover Job would be rejected by the API server later with a far less actionable message.contextnames the backend (e.g."s3 backend") for the message. - validate_
backup - Validate a
Snapshotspec for a given origin, accumulating all problems. - validate_
backup_ config - Validate a
SnapshotPolicyspec, accumulating all problems. - validate_
backup_ deletion_ policy - A
Snapshot’sdeletionPolicyis legal for its origin (ADR §4.5). - validate_
backup_ on_ schedule_ delete origin: discoveredSnapshots carry an empty spec; a stamped cascade policy on one is meaningless (their owner is a repository, not a schedule) and forbidden, like a non-Retain deletionPolicy.origin: adoptedis forbidden for the same reason: an adopted row’s owner is theSnapshotPolicyit was re-attached to, never aSnapshotSchedule.- validate_
backup_ schedule - Validate a
SnapshotSchedulespec, accumulating all problems. - validate_
catalog_ bounds - Validate
spec.catalog(ADR §3.1/§3.2): the refresh interval must parse and respect the floor, the retain bounds must be enforceable, andfallbackNamespaceonly means something on a cluster-scoped repository (cluster_scoped). One validator for both kinds so the rules cannot fork. - validate_
cluster_ name - Validate a
Repository/ClusterRepositoryidentityDefaults.cluster: an RFC 1123 label (^[a-z0-9]([a-z0-9-]*[a-z0-9])?$), 1..=CLUSTER_NAME_MAX_LENcharacters, with dots called out explicitly as forbidden even though a well-formed RFC 1123 label never contains one anyway — the message needs to explain why to whoever hits it:clusteris concatenated onto a namespace as<namespace>.<cluster>for the default hostname (seecrate::identity::resolve_identity), andcrate::identity::classify_hostnamesplits that hostname back apart at the FIRST., so a dot anywhere inclusterwould make that split ambiguous. - validate_
cluster_ repository - Validate a
ClusterRepositoryspec, accumulating all problems (ADR §3.2). - validate_
cluster_ repository_ immutability - Reject changes to create-time-immutable
ClusterRepositoryfields on UPDATE (ADR-0005 §7). Same field set asvalidate_repository_immutability. - validate_
consumer_ against_ cluster_ repo - A consumer namespace is permitted by a
ClusterRepository’s tenancy gate (ADR §3.2/§4.3). - validate_
cron - A cron expression parses with the same parser the controller uses at runtime, so bad expressions are rejected at apply time, not at first reconcile (ADR §4.1).
- validate_
dns1123_ name - A DNS-1123 subdomain (the shape of every Kubernetes object name): non-empty,
≤253 chars, lowercase alphanumerics /
-/., starting and ending alphanumeric. The structural schema can’t express it, so the webhook does.fieldnames where the value appears, for an actionable message. - validate_
failure_ policy - Validate a
FailurePolicy’s numeric fields are sane:activeDeadlineSecondsandpodStartupDeadlineSecondsmust be positive (the kubelet rejects a non-positive Job deadline, and a non-positive grace would fail every pod on its first reconcile);backoffLimitmust be non-negative.contextnames the owner (e.g."Snapshot"). - validate_
foreign_ snapshots_ cluster_ coupling - The
identityDefaults.cluster×catalog.foreignSnapshotscross-field rules (multi-cluster shared-repo): classifying a snapshot as another cluster’s is undecidable without a cluster identity (a), and adopting one must never silently switch off an already-configured fallback collector (d). Shared by both repository kinds —clusteris the resolvedidentityDefaults.clustervalue,Nonewhen the repository has no cluster identity set (or, on a namespacedRepository, noidentityDefaultsset at all). Without a cluster, rule (d) is a no-op (it requires one to fire) while rule (a) still rejects anyforeignSnapshotsset there. - validate_
identity_ component - Validate a resolved kopia identity component (
username/hostname). Shape-only (see [identity_char_problem]);fieldnames the surface for the message. Called both from the static admission validator (on explicit overrides) and fromcrate::resolve_identity(on the fully-resolved value, covering CEL results and defaults), so a bad identity can never be pinned. - validate_
jitter - Validate an optional Go-style
jitterduration (30m,1h, …) against the SAME parser the controller uses at scheduling time, so a typo or an out-of-range value is rejected at apply time rather than silently degrading to no jitter at the next reconcile (parse_go_durationreturnsNone, which the schedule treats as a zero offset).None(no jitter) is always valid.fieldnames the path for the error message (e.g.spec.schedule.jitter). - validate_
maintenance - Validate a
Maintenancespec, accumulating all problems (ADR §3.7). - validate_
mover - Validate a
MoverSpec.contextnames the owning resource for the message (e.g."Restore mover"). - validate_
nfs_ volume - An inline
NfsVolumeis well-formed: a non-empty server and an absolute export path. The structural schema can’t express either, so the webhook does.contextnames where it appears (e.g."snapshot source","filesystem repo") for an actionable message. - validate_
replication_ auth - A
RepositoryReplication’s source/destination auth pair is safe to run in one mover pod. The replicate pod’s environment carries the static side’s credential Secret (envFrom); for a same-kind S3 or Azure pair where exactly one side uses workload identity, the workload-identity side’s credential chain reads those same env vars (minio-go’sEnvAWS; kopia’s env-bound azure flags) and would silently authenticate as the other side — wrong identity, plausibly wrong permissions, no error. Rejected at admission instead. GCS mixed pairs are safe (the static side’s key travels as a--credentials-filepath, not ambient env). Both-workload-identity pairs must name the same ServiceAccount — a pod runs as exactly one. - validate_
replication_ destination_ secret_ namespace - A
RepositoryReplication’s destination credential Secret is reachable from the mover Job. The replicate Job runs in the CR’s own namespace and loads the destination backend’s keys viaenvFrom, which is namespace-local — a Secret in another namespace can never be read.RepositoryReplicationdeliberately has nocredentialProjection, so an out-of-namespace destinationauth.secretRefis a dead reference the Job would hang on (CreateContainerConfigError). Reject it at admission with an actionable message instead. An absentnamespacemeans “same namespace as the CR” and is always legal; a workload- identity or filesystem destination carries no auth Secret and is unaffected.cr_namespaceis the replication CR’s own namespace. - validate_
repository - Validate a
Repositoryspec, accumulating all problems (ADR §3.1). - validate_
repository_ health spec.healthrules shared byRepositoryandClusterRepository(ADR-0005 §13). The index-blob warning threshold must be non-negative: a negative count is nonsensical, and0is the documented sentinel that disables the warning (so it is allowed).contextnames the kind for the message (“Repository” / “ClusterRepository”).- validate_
repository_ immutability - Reject changes to create-time-immutable
Repositoryfields on UPDATE (ADR-0005 §7):create.splitter,create.hash,create.encryption,create.ecc. Returns every changed field so a user sees them all at once. Empty ⇒ no immutable change. - validate_
repository_ maintenance - Validate a
spec.maintenanceblock on aRepository/ClusterRepository, accumulating problems (ADR §3.7): - validate_
repository_ no_ inline_ retention - A
Repositoryspec does not carry kopia-side (repo-level) retention policy, which would conflict with CR-driven GFS retention (ADR §4.4 exclusivity). - validate_
repository_ parameters spec.parametersis well-formed and applicable (#258). Shared by both repository kinds viacontext, exactly likevalidate_repository_health.- validate_
repository_ ref - A
RepositoryRefis well-formed: aClusterRepositoryreference is by name only, sonamespaceMUST be absent (ADR §3.2/§3.3). A namespacedRepositoryreference may carry a namespace (cross-namespace references are allowed). - validate_
repository_ replication - Validate a
RepositoryReplicationspec, accumulating all problems (ADR-0005 §13(d)): thesourceRefis well-formed, the schedule cron parses, the destination backend’s content is valid, and (when a mover is set) it’s well-formed. The “destination differs from source” rule needs the resolved source backend, which this pure validator cannot fetch — the webhook resolves it and callsreplication_destination_differsseparately. - validate_
resources - Validate that a
ResourceRequirementshas norequests > limitsfor any key. A pod withrequests > limitsis rejected by the API server, so the mover Job never creates a pod and the run hangs — the same silent-wedge class as an impossible securityContext.contextnames the owner (e.g."SnapshotPolicy mover"). - validate_
restore - A
Restorespec is internally consistent (ADR §3.6/§4.6 / ADR-0005 §9). - validate_
restore_ spec - Validate a
Restorespec, accumulating all problems (wraps the fail-fastvalidate_restorefor caller symmetry). - validate_
schedule_ policy_ target - Exactly one of
policyRef/policySelectoris set on aSnapshotSchedule(ADR-0005 §10). Neither ⇒MissingRequiredField; both ⇒MutuallyExclusive. Pure so the XOR decision is unit-tested directly. - validate_
server - The shared
spec.serverrules the type system can’t express (server addendum): - validate_
snapshot_ tags - Validate
Snapshot.spec.tags(admission): every key/value must passsnapshot_tag_errorand the map is bounded toMAX_SNAPSHOT_TAGSentries. Accumulates every problem, one error per offending tag. - validate_
source - A single backup
Sourceis well-formed: exactly one ofpvc,pvcSelector, ornfsis set (ADR §3.3 — modeled as sibling Options because the forms sharesourcePath*keys, so it’s a webhook check, not an enum). When the source isnfs, its server/path are also validated. - validate_
source_ path - Validate a kopia identity
sourcePath(the part after the first:). Lenient: spaces and:are allowed (only the first:is kopia’s delimiter, and the rest is the path verbatim), but the path must be non-empty and free of newlines / ASCII control characters. - validate_
timezone - Validate an optional IANA timezone name against the same
chrono-tzdatabase the controller uses at scheduling time, so a typo (e.g.America/Chicgo) is rejected at apply time rather than silently resolving to UTC at the next reconcile.None(use the controller default) is always valid.