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§

CollisionHit
A detected identity collision, naming WHICH (identity, repository) pair collided — with a multi-repository policy contributing N pairs, the deny message must say which member repository is the problem (the others may be perfectly fine). Produced by detect_identity_collision_multi.
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.

Enums§

AuthPairKind
WHICH one-pod credential pairing validate_replication_auth is judging.
NumericBound
The semantic class of a numeric lower-bound, so require_min can attach the right minimum AND a one-line why without every caller re-deriving it — the difference between the old terse "must be >= 1 (got 0)" and a message that says why 0 is refused and what to do instead. Exhaustive min/because, so a new bound cannot be added without deciding both (mirrors the type-safety thesis: a knob’s meaning is not free-text).

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_JITTER
The largest jitter window any cron may carry: 24 hours.
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).
S3_TLS_SKIP_VERIFY_WARNING
The actionable admission warning for an S3 backend pairing tls.caBundleRef with tls.insecureSkipVerify: true: kopia’s --disable-tls-verification wins, so the referenced CA bundle is ignored while insecureSkipVerify is set. Deliberately a WARNING, never a hard error — the ClusterRepository and RepositoryReplication reconcilers defensively re-validate the FULL spec on every reconcile and hard-error on failure, so promoting this combination to an error would brick every already-persisted CR carrying it on operator upgrade, with no admission request in flight for a user to react to. (The caBundleRef + disableTls contradiction IS a hard error, but only because caBundleRef never worked at all before this validation existed — no working CR can carry 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_collision_multi
The N-pair generalization of detect_identity_collision: the candidate policy contributes one (identity, repo_key) pair per member repository (each identity resolved under THAT repository’s identityDefaults), and a collision is the FIRST pair that matches an existing pair. Loops the single-pair kernel, so N = 1 is exactly the old behavior.
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_identity_fork_multi
The per-repository generalization of detect_identity_fork for multi-repository policies (plan B5). The unit of identity is the (repo_key, identity-under-that-repo's-identityDefaults) pair, so:
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).
replication_filesystem_mount_collision
Two DISTINCT filesystem repositories that share the same in-pod backend.path cannot ride one replication mover pod: the Job mounts each repo’s volume at its own path, and two volumeMounts at one mountPath make the pod spec invalid — a failure that would otherwise surface only as a reconcile-time Job-create error. [backend_target_key] deliberately keys filesystem targets by (path, volume) so this pair PASSES the self-target check (different volumes = genuinely different repos); the mount collision is a separate, mover-topology constraint, so it gets its own guard.
replication_identity_overlap
The destination-side identities (rendered as kopia’s username@hostname[:path]) that include/exclude would select — i.e. identities this replication would copy INTO while a destination policy also writes them directly. Empty means no overlap. Sorted and de-duplicated so the admission message (and any test) is deterministic.
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) and the S3 caBundleRef + insecureSkipVerify shadowing (see S3_TLS_SKIP_VERIFY_WARNING). Takes the resolved backend + moverDefaults so it serves both kinds without re-deriving them.
require_min
A numeric knob must meet its NumericBound’s minimum — the shared one-liner behind every Option<u32> count / Option<i64> rate field (e.g. RepositoryReplication.spec.sync.parallel), so the rule, its minimum, AND its rationale 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, Azure’s workload-identity prerequisites, and the S3 tls block’s consistency. 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_backend_tls
A backend’s tls block is internally consistent — every rule the structural schema can’t express. A caBundleRef must actually name a ConfigMap (configMapName was Option for API growth, so an empty caBundleRef: {} parses fine but would be a silently dead reference), the name must be a valid object name (or every mover run fails at ConfigMap resolution with a far less actionable message), an explicitly-set key must not be blank (blank would shadow the ca.crt default and never match a real key), and pairing caBundleRef with disableTls: true is a contradiction: with kopia’s --disable-tls there is no TLS handshake at all, so the CA could never be consulted. 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_config_admission_extras
Admission-only extras for a SnapshotPolicy: the jitter 24h cap on both verification tiers. The PARSE half already lives in validate_backup_config (and has since these fields shipped); only the bound is new, so only the bound is admission-only.
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. So is origin: replicated: a copy CR belongs to its SnapshotReplication, and no SnapshotSchedule ever fires (or cascades onto) one.
validate_backup_schedule
Validate a SnapshotSchedule spec, accumulating all problems.
validate_backup_schedule_admission_extras
Admission-only extras for a SnapshotSchedule: the jitter 24h cap and the non-negative startingDeadlineSeconds rule. Both TIGHTEN fields that already exist, so neither may join validate_backup_schedule.
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_jitter_bounds
Validate a present Go-style jitter duration: it must parse (same parser validate_jitter and the scheduler use) AND must not exceed MAX_JITTER.
validate_maintenance
Validate a Maintenance spec, accumulating all problems (ADR §3.7).
validate_maintenance_admission_extras
Admission-only extras for a Maintenance: parse AND bound both jitter windows.
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_pod_metadata
Validate moverDefaults.podLabels/podAnnotations key sets, accumulating every reserved key so a user fixes them all in one apply.
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_repository_replication_admission_extras
Admission-only extras for a RepositoryReplication: parse AND bound the schedule jitter. Same history as Maintenancevalidate_repository_replication checks the cron and timezone but never the jitter, so both halves are new rejections over a field stored objects already carry.
validate_repository_seed
Validate spec.seed on a Repository/ClusterRepository, accumulating every independent problem.
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_seed_not_self
A migrate-mode seed must not point at the repository being defined. Uses the shared crate::common::repo_key normalization, so a Repository naming its own namespace explicitly and one omitting it are both caught. Needs the CR’s own identity, which the spec does not carry.
validate_seed_secret_namespace
The namespaced arm of the co-resident seed-Secret rule: a Repository’s blob-mode seed source Secret must be unset or name the repository’s OWN namespace, because the seeding Job runs there and envFrom is namespace-local. Needs the CR’s namespace, which the spec does not carry, so the webhook calls it with req.namespace (the cluster-scoped arm — “must be unset” — is fully spec-derivable and lives in validate_repository_seed).
validate_server
The shared spec.server rules the type system can’t express (server addendum):
validate_snapshot_replication
Validate a SnapshotReplication spec, accumulating all problems (issue #368): both repository refs are well-formed and not literally the same reference, the schedule cron/timezone/jitter parse, every identity matcher sets at least one component and every set component is a compilable glob, migrate.parallel is >= 1, a retention pruning keeps at least something, and (when a mover is set) it’s well-formed and does not claim inheritSecurityContextFrom. The “two different refs resolving to the same storage” rule needs both resolved backends, which this pure validator cannot fetch — the webhook resolves them and compares [backend_target_key]s.
validate_snapshot_replication_admission_extras
Admission-only extras for a SnapshotReplication: the jitter 24h cap. The parse half already lives in validate_snapshot_replication.
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_throttle
Validate every SET knob of a Throttle: a cap is a positive rate, so each present field must be >= 1. field names the block’s path for the message (e.g. "SnapshotReplication spec.migrate.throttle.source"), and each knob’s own camelCase name is appended.
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.