Skip to main content

Module validate

Module validate 

Source
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§

ExistingIdentity
An already-admitted SnapshotPolicy’s identity, keyed for collision detection (ADR-0005 §6). repo_key is a normalized repository identity (e.g. "ClusterRepository/shared" or "Repository/backups/nas") so two policies are “the same repository” only when their keys match; name is the policy’s namespace/name for the actionable message.

Constants§

CLUSTER_NAME_MAX_LEN
Maximum length of Repository/ClusterRepository identityDefaults.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 moverDefaults grant write access only via fsGroup. fsGroup is 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 repo connect/create fails with permission 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 conflicting namespace/name or None.
detect_identity_fork
Pure decision for the fork-on-edit guard on a username@hostname change. Returns Some(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 a Repository/ClusterRepository’s identityDefaults (cluster, hostnameExpr, or usernameExpr) changes what every consumer SnapshotPolicy relying on those defaults resolves to — silently, with no per-policy edit to acknowledge it (unlike detect_identity_fork, which guards a policy’s own edit). Returns Some(ValidationError::RepositoryIdentityWouldFork) iff identityDefaults actually changed, at least one consumer has snapshot history, and the change is not acknowledged. The webhook does the IO (list consumer SnapshotPolicys, 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 sourcePathOverride on 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 destination backend 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 (see NFS_FSGROUP_WARNING). Takes the resolved backend + moverDefaults so it serves both kinds without re-deriving them.
require_min
A numeric knob must be at least min — the shared one-liner behind every Option<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. field names the exact path for the message (e.g. "RepositoryReplication spec.sync.parallel"). Callers only invoke this for a Some value — 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 Snapshot CR per source, and they accumulate up to the SnapshotPolicy retention 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.tags key/value pair is invalid, or None when 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 Filesystem backend, the secretRef XOR workloadIdentity rule on the cloud-IAM backends, and Azure’s workload-identity prerequisites. Exhaustive match so a new Backend variant must be considered here before it compiles.
validate_backend_auth
A cloud-IAM backend’s auth block is well-formed: exactly one of secretRef or workloadIdentity when auth is present (both are Option because the forms share the auth key, so it’s a webhook check — the same shape as validate_source). An absent/empty auth is legal: the well-known keys may ride the encryption-password Secret, and an empty block means exactly that. A workload-identity serviceAccountName must be a valid object name, or the mover Job would be rejected by the API server later with a far less actionable message. context names the backend (e.g. "s3 backend") for the message.
validate_backup
Validate a Snapshot spec for a given origin, accumulating all problems.
validate_backup_config
Validate a SnapshotPolicy spec, accumulating all problems.
validate_backup_deletion_policy
A Snapshot’s deletionPolicy is legal for its origin (ADR §4.5).
validate_backup_on_schedule_delete
origin: discovered Snapshots 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: adopted is forbidden for the same reason: an adopted row’s owner is the SnapshotPolicy it was re-attached to, never a SnapshotSchedule.
validate_backup_schedule
Validate a SnapshotSchedule spec, 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, and fallbackNamespace only 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/ClusterRepository identityDefaults.cluster: an RFC 1123 label (^[a-z0-9]([a-z0-9-]*[a-z0-9])?$), 1..=CLUSTER_NAME_MAX_LEN characters, 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: cluster is concatenated onto a namespace as <namespace>.<cluster> for the default hostname (see crate::identity::resolve_identity), and crate::identity::classify_hostname splits that hostname back apart at the FIRST ., so a dot anywhere in cluster would make that split ambiguous.
validate_cluster_repository
Validate a ClusterRepository spec, accumulating all problems (ADR §3.2).
validate_cluster_repository_immutability
Reject changes to create-time-immutable ClusterRepository fields on UPDATE (ADR-0005 §7). Same field set as validate_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. field names where the value appears, for an actionable message.
validate_failure_policy
Validate a FailurePolicy’s numeric fields are sane: activeDeadlineSeconds and podStartupDeadlineSeconds must be positive (the kubelet rejects a non-positive Job deadline, and a non-positive grace would fail every pod on its first reconcile); backoffLimit must be non-negative. context names the owner (e.g. "Snapshot").
validate_foreign_snapshots_cluster_coupling
The identityDefaults.cluster × catalog.foreignSnapshots cross-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 — cluster is the resolved identityDefaults.cluster value, None when the repository has no cluster identity set (or, on a namespaced Repository, no identityDefaults set at all). Without a cluster, rule (d) is a no-op (it requires one to fire) while rule (a) still rejects any foreignSnapshots set there.
validate_identity_component
Validate a resolved kopia identity component (username/hostname). Shape-only (see [identity_char_problem]); field names the surface for the message. Called both from the static admission validator (on explicit overrides) and from crate::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 jitter duration (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_duration returns None, which the schedule treats as a zero offset). None (no jitter) is always valid. field names the path for the error message (e.g. spec.schedule.jitter).
validate_maintenance
Validate a Maintenance spec, accumulating all problems (ADR §3.7).
validate_mover
Validate a MoverSpec. context names the owning resource for the message (e.g. "Restore mover").
validate_nfs_volume
An inline NfsVolume is well-formed: a non-empty server and an absolute export path. The structural schema can’t express either, so the webhook does. context names 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’s EnvAWS; 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-file path, 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 via envFrom, which is namespace-local — a Secret in another namespace can never be read. RepositoryReplication deliberately has no credentialProjection, so an out-of-namespace destination auth.secretRef is a dead reference the Job would hang on (CreateContainerConfigError). Reject it at admission with an actionable message instead. An absent namespace means “same namespace as the CR” and is always legal; a workload- identity or filesystem destination carries no auth Secret and is unaffected. cr_namespace is the replication CR’s own namespace.
validate_repository
Validate a Repository spec, accumulating all problems (ADR §3.1).
validate_repository_health
spec.health rules shared by Repository and ClusterRepository (ADR-0005 §13). The index-blob warning threshold must be non-negative: a negative count is nonsensical, and 0 is the documented sentinel that disables the warning (so it is allowed). context names the kind for the message (“Repository” / “ClusterRepository”).
validate_repository_immutability
Reject changes to create-time-immutable Repository fields 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.maintenance block on a Repository/ClusterRepository, accumulating problems (ADR §3.7):
validate_repository_no_inline_retention
A Repository spec 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.parameters is well-formed and applicable (#258). Shared by both repository kinds via context, exactly like validate_repository_health.
validate_repository_ref
A RepositoryRef is well-formed: a ClusterRepository reference is by name only, so namespace MUST be absent (ADR §3.2/§3.3). A namespaced Repository reference may carry a namespace (cross-namespace references are allowed).
validate_repository_replication
Validate a RepositoryReplication spec, accumulating all problems (ADR-0005 §13(d)): the sourceRef is 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 calls replication_destination_differs separately.
validate_resources
Validate that a ResourceRequirements has no requests > limits for any key. A pod with requests > limits is 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. context names the owner (e.g. "SnapshotPolicy mover").
validate_restore
A Restore spec is internally consistent (ADR §3.6/§4.6 / ADR-0005 §9).
validate_restore_spec
Validate a Restore spec, accumulating all problems (wraps the fail-fast validate_restore for caller symmetry).
validate_schedule_policy_target
Exactly one of policyRef / policySelector is set on a SnapshotSchedule (ADR-0005 §10). Neither ⇒ MissingRequiredField; both ⇒ MutuallyExclusive. Pure so the XOR decision is unit-tested directly.
validate_server
The shared spec.server rules the type system can’t express (server addendum):
validate_snapshot_tags
Validate Snapshot.spec.tags (admission): every key/value must pass snapshot_tag_error and the map is bounded to MAX_SNAPSHOT_TAGS entries. Accumulates every problem, one error per offending tag.
validate_source
A single backup Source is well-formed: exactly one of pvc, pvcSelector, or nfs is set (ADR §3.3 — modeled as sibling Options because the forms share sourcePath* keys, so it’s a webhook check, not an enum). When the source is nfs, 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-tz database 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.