187 lines
7.8 KiB
Text
187 lines
7.8 KiB
Text
|
|
# Backup policy contracts — declarative description of how a component is backed up.
|
||
|
|
# Consumed by the backup-manager crate (one-shot, daemon, standalone, coordinator modes).
|
||
|
|
# Encryption, multi-destination replication and non-empty scopes are enforced as
|
||
|
|
# Nickel contracts so misconfiguration fails at `nickel export` time, not at runtime.
|
||
|
|
|
||
|
|
let vault = import "vault_refs.ncl" in
|
||
|
|
|
||
|
|
let _Duration = std.contract.from_validator (fun value =>
|
||
|
|
if !(std.is_string value)
|
||
|
|
then 'Error { message = "Duration must be a String" }
|
||
|
|
else if std.string.length value == 0
|
||
|
|
then 'Error { message = "Duration must be non-empty" }
|
||
|
|
else if !(std.string.contains "s" value
|
||
|
|
|| std.string.contains "m" value
|
||
|
|
|| std.string.contains "h" value
|
||
|
|
|| std.string.contains "d" value)
|
||
|
|
then 'Error { message = "Duration must contain a time unit (s, m, h, d)" }
|
||
|
|
else 'Ok
|
||
|
|
) in
|
||
|
|
|
||
|
|
let _CronExpr = std.contract.from_validator (fun value =>
|
||
|
|
if !(std.is_string value)
|
||
|
|
then 'Error { message = "CronExpr must be a String" }
|
||
|
|
else
|
||
|
|
let parts = std.string.split " " value in
|
||
|
|
if std.array.length parts == 5 || std.array.length parts == 6
|
||
|
|
then 'Ok
|
||
|
|
else 'Error { message = "CronExpr must have 5 or 6 space-separated fields" }
|
||
|
|
) in
|
||
|
|
|
||
|
|
let _Tags = { _ | String } in
|
||
|
|
|
||
|
|
let _DnsRecordsLike = { .. } in
|
||
|
|
|
||
|
|
{
|
||
|
|
Duration = _Duration,
|
||
|
|
CronExpr = _CronExpr,
|
||
|
|
Tags = _Tags,
|
||
|
|
|
||
|
|
# Schedule discriminated union: cron, interval or NATS event-driven.
|
||
|
|
Schedule = {
|
||
|
|
kind | [| 'cron, 'interval, 'on_event |],
|
||
|
|
cron_expr | _CronExpr | optional,
|
||
|
|
jitter_sec | Number | optional,
|
||
|
|
every | _Duration | optional,
|
||
|
|
jitter | _Duration | optional,
|
||
|
|
subject | String | optional | doc "NATS subject when kind = 'on_event",
|
||
|
|
debounce | _Duration | optional,
|
||
|
|
},
|
||
|
|
|
||
|
|
# Retention preset — keeps last N + N daily/weekly/monthly/yearly snapshots.
|
||
|
|
RetentionPolicy = {
|
||
|
|
keep_last | Number | default = 7,
|
||
|
|
keep_daily | Number | default = 7,
|
||
|
|
keep_weekly | Number | default = 4,
|
||
|
|
keep_monthly | Number | default = 6,
|
||
|
|
keep_yearly | Number | default = 0,
|
||
|
|
prune_after | _Duration | optional | doc "Delete data older than this regardless of keep_* (safety bound)",
|
||
|
|
},
|
||
|
|
|
||
|
|
# A backup destination (where snapshots end up). At least 2 are required
|
||
|
|
# when policy is enabled (MultiDestinationRequired contract).
|
||
|
|
Destination = {
|
||
|
|
name | String | doc "Stable identifier (used in metrics labels and tags)",
|
||
|
|
kind | [| 's3, 'b2, 'local, 'sftp, 'rest_server |],
|
||
|
|
uri | String | doc "restic-style URI: 's3:host/bucket', 'b2:bucket', 'sftp:user@host:/path', etc.",
|
||
|
|
cred_ref | vault.VaultCredRef,
|
||
|
|
role | [| 'primary, 'replica, 'archive |] | default = 'replica,
|
||
|
|
region | String | optional,
|
||
|
|
},
|
||
|
|
|
||
|
|
# Tagging strategy for snapshots. The actual tags emitted are a determinístic
|
||
|
|
# function of {component, scope, parameters} computed by the manager.
|
||
|
|
TagStrategy = {
|
||
|
|
component_label | String | doc "Used as `component=<value>` tag",
|
||
|
|
extra | Array String | doc "Additional static tags (k=v strings)" | default = [],
|
||
|
|
},
|
||
|
|
|
||
|
|
# Database dump strategy. Three flavours cover the consistency/atomicity matrix.
|
||
|
|
DumpStrategy = {
|
||
|
|
kind | [| 'stream_to_stdin, 'dump_to_path, 'pre_dump_then_path,
|
||
|
|
'csi_volume_snapshot, 'app_quiesce_then_snapshot |],
|
||
|
|
dump_command | String | optional,
|
||
|
|
path | String | optional,
|
||
|
|
cleanup | Bool | default = true,
|
||
|
|
volume | String | optional,
|
||
|
|
snapshot_class | String | optional,
|
||
|
|
quiesce_cmd | String | optional,
|
||
|
|
unquiesce_cmd | String | optional,
|
||
|
|
},
|
||
|
|
|
||
|
|
DbEngine = [| 'postgresql, 'mariadb, 'mysql, 'redis, 'mongodb, 'surrealdb, 'etcd, 'sqlite |],
|
||
|
|
|
||
|
|
# Discriminated scope: what gets backed up and how it's grouped/tagged.
|
||
|
|
BackupScope = {
|
||
|
|
kind | [| 'service_full, 'per_domain, 'per_mailbox, 'database,
|
||
|
|
'volume_snapshot, 'logs_archive, 'kv_export |],
|
||
|
|
name | String | doc "Identifier within a policy (used in CLI: --scope <name>)",
|
||
|
|
paths | Array String | default = [],
|
||
|
|
exclude | Array String | default = [],
|
||
|
|
domains | Array String | default = [],
|
||
|
|
base_path | String | default = "",
|
||
|
|
selector | String | optional,
|
||
|
|
engine | DbEngine | optional,
|
||
|
|
dump_strategy | DumpStrategy | optional,
|
||
|
|
volumes | Array String | default = [],
|
||
|
|
snapshot_class | String | optional,
|
||
|
|
sources | Array String | default = [],
|
||
|
|
format | [| 'jsonl_gz, 'tar_gz, 'restic_native, 'sqlite_dump |] | optional,
|
||
|
|
rotation | _Duration | optional,
|
||
|
|
source | [| 'etcd, 'consul, 'loki, 'journald, 'files |] | optional,
|
||
|
|
tag_prefix | String | default = "",
|
||
|
|
tags | _Tags | default = {},
|
||
|
|
},
|
||
|
|
|
||
|
|
# Pre/post hooks executed by the manager around the backup run.
|
||
|
|
Hooks = {
|
||
|
|
pre | Array String | default = [],
|
||
|
|
post | Array String | default = [],
|
||
|
|
timeout | _Duration | default = "5m",
|
||
|
|
abort_on_failure | Bool | default = true,
|
||
|
|
},
|
||
|
|
|
||
|
|
# Throttle network bandwidth (passed to provider as --limit-upload/--limit-download).
|
||
|
|
Throttle = {
|
||
|
|
upload_kbps | Number | optional,
|
||
|
|
download_kbps | Number | optional,
|
||
|
|
},
|
||
|
|
|
||
|
|
# Verify policy. Drill is a separate spec consumed by verify_policy.ncl.
|
||
|
|
VerifyPolicyRef = {
|
||
|
|
schedule | { kind | [| 'cron, 'interval |], cron_expr | String | optional, every | _Duration | optional } | optional,
|
||
|
|
level | [| 'quick, 'deep, 'restore_drill, 'full_dr |] | default = 'quick,
|
||
|
|
drill_ref | String | optional | doc "Reference to a DrillSpec by name (looked up from verify-recipes/)",
|
||
|
|
},
|
||
|
|
|
||
|
|
# Provider reference. Manager resolves to extensions/providers/backup/<name>/.
|
||
|
|
BackupProviderRef = {
|
||
|
|
name | String | doc "Provider directory name (e.g. 'restic', 'kopia')",
|
||
|
|
version | String | optional | doc "Pinned version; warn if installed CLI mismatches",
|
||
|
|
},
|
||
|
|
|
||
|
|
# === Contracts ===========================================================
|
||
|
|
|
||
|
|
# NonEmptyScopes: an enabled BackupPolicy must have at least one scope.
|
||
|
|
NonEmptyScopes = std.contract.from_validator (fun value =>
|
||
|
|
if std.array.length value > 0
|
||
|
|
then 'Ok
|
||
|
|
else 'Error { message = "BackupPolicy.scopes must contain at least one BackupScope" }
|
||
|
|
),
|
||
|
|
|
||
|
|
# MultiDestinationRequired: enforces the off-site replication invariant.
|
||
|
|
# A policy must declare ≥2 destinations and at least one with role = 'primary.
|
||
|
|
MultiDestinationRequired = std.contract.from_validator (fun value =>
|
||
|
|
if std.array.length value < 2
|
||
|
|
then 'Error {
|
||
|
|
message = "BackupPolicy.destinations must contain at least 2 entries (off-site replication is non-negotiable)",
|
||
|
|
}
|
||
|
|
else
|
||
|
|
let has_primary = std.array.any (fun d => d.role == 'primary) value in
|
||
|
|
if !has_primary
|
||
|
|
then 'Error {
|
||
|
|
message = "BackupPolicy.destinations must contain at least one entry with role = 'primary",
|
||
|
|
}
|
||
|
|
else 'Ok
|
||
|
|
),
|
||
|
|
|
||
|
|
# === Top-level policy ====================================================
|
||
|
|
|
||
|
|
BackupPolicy = {
|
||
|
|
provider | BackupProviderRef,
|
||
|
|
destinations | Array Destination
|
||
|
|
| doc "≥2 destinations, at least one 'primary",
|
||
|
|
encryption | vault.VaultKeyRef
|
||
|
|
| doc "Encryption key reference in vault (E2E encryption is non-negotiable)",
|
||
|
|
schedule | Schedule,
|
||
|
|
retention | RetentionPolicy,
|
||
|
|
scopes | Array BackupScope
|
||
|
|
| doc "1..N backup units; tagged determinístically",
|
||
|
|
tag_strategy | TagStrategy,
|
||
|
|
hooks | Hooks | optional,
|
||
|
|
verify | VerifyPolicyRef | optional,
|
||
|
|
throttle | Throttle | optional,
|
||
|
|
consistency_group | String | optional | doc "If set, this policy participates in a BackupGroup",
|
||
|
|
},
|
||
|
|
}
|