Help us shape the future of Go! Take the 2026 Go Developer Survey

snapshot

package
v0.37.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 32 Imported by: 0

Documentation

Overview

Package snapshot orchestrates btrfs snapshot tooling (btrbk, systemd) through a single declarative TOML config. bentoolkit is an orchestrator, not a btrfs implementation: it renders native config for mature tools and coordinates them; no destructive btrfs logic is written in Go.

This file holds the config model and path resolution. The four orchestration interfaces (Engine, Shipper, Notifier, Scheduler) live in their own files, the concrete drivers in engine_btrbk.go / ship_ssh.go / systemd.go, and the run pipeline in manager.go.

Index

Constants

View Source
const (
	StageCreate = "create"
	StagePrune  = "prune"
	StageShip   = "ship"
	StageGFS    = "gfs"
)

Stage names recorded in a RunResult, in pipeline order. StageGFS is the on-demand remote GFS sweep of `snapshot prune` — distinct from StagePrune (the engine-local prune) so a RunResult tells the two apart.

View Source
const (
	StatusOK     = "ok"
	StatusFailed = "failed"
)

Stage outcome statuses.

View Source
const DefaultBtrbkConfPath = "/etc/bentoo/btrbk.conf"

DefaultBtrbkConfPath is where `apply` writes the rendered btrbk.conf and where the engine points btrbk via `-c`.

Variables

View Source
var EmergeHookRoot = "/"

EmergeHookRoot is the filesystem root under which the Portage emerge-hook paths are WRITTEN (hook script + bashrc). It is "/" in production; tests point it at a temp dir — mirroring the exported StateDir seam in result.go.

Note the write/runtime distinction: EmergeHookRoot only prefixes where files are written. The managed bashrc block always sources the hook script's absolute RUNTIME path (/etc/portage/bashrc.d/...), never the prefixed one, because that is where Portage will find it on the running system.

View Source
var ErrBrokenChain = errors.New("archive restore chain is broken")

ErrBrokenChain is returned when an archive restore's delta chain is not a contiguous full→…→target sequence — an empty chain, a first link that is not a full, or a gap where a delta's parent is missing. It is the backstop: a broken chain is refused BEFORE any `btrfs receive` runs, so a restore can never apply a delta whose base is absent.

View Source
var ErrBrokenHookBlock = errors.New("bentoo snapshot hook block has no end marker")

ErrBrokenHookBlock marks a bashrc whose managed-block begin marker has no end marker after it. Stripping such a block would drop every line to the end of the file, so install and uninstall refuse it instead.

View Source
var ErrDriverUnavailable = errors.New("snapshot driver dependency missing")

ErrDriverUnavailable is returned when an active driver's binary is absent from PATH. The message names the Portage package to install.

View Source
var ErrEngineFailed = errors.New("snapshot engine command failed")

ErrEngineFailed wraps a non-zero exit from the underlying engine binary (e.g. `btrbk run`). Stage errors in a RunResult are built by joining this with the captured stderr.

View Source
var ErrInvalidConfigValue = errors.New("invalid snapshot config value")

ErrInvalidConfigValue is returned by Validate when a value that reaches btrbk.conf or a systemd unit carries a control character. A newline there would inject a directive of the file's own grammar.

View Source
var ErrInvalidDriver = errors.New("invalid snapshot driver")

ErrInvalidDriver is returned by the factories (and by Config.Validate) when an engine.driver, ship.type, or schedule.backend names an unknown driver. It is the single shared sentinel for "unknown driver string" across the package, mirroring provider.ErrInvalidProvider.

View Source
var ErrInvalidShipTarget = errors.New("invalid ssh ship target")

ErrInvalidShipTarget is returned when an ssh ship entry has a missing or malformed target (expected user@host:/path).

View Source
var ErrRestoreDeclined = errors.New("restore declined by operator")

ErrRestoreDeclined is returned when the operator does not approve a destructive restore at the confirm prompt. When this is returned, NOTHING has been applied — the gate fires before any subprocess.

View Source
var ErrRollbackDeclined = errors.New("rollback declined by operator")

ErrRollbackDeclined is returned when the operator does not approve a destructive rollback at the confirm prompt. When this is returned, NOTHING has run — the gate fires before any subprocess. Mirrors ErrRestoreDeclined.

View Source
var ErrRollbackUnsupported = errors.New("rollback requires the snapper engine")

ErrRollbackUnsupported is returned when the active engine is not snapper: `snapper rollback` has no btrbk equivalent, so rollback is refused outright with a clear message rather than approximated.

View Source
var ErrSchedulerFailed = errors.New("snapshot scheduler command failed")

ErrSchedulerFailed wraps a non-zero exit from systemctl.

View Source
var ErrShipEngineMismatch = errors.New("ship type not supported by engine")

ErrShipEngineMismatch is returned by Validate when a ship type cannot work with the configured engine: ssh shipping is btrbk's own transfer, so snapper cannot perform it.

View Source
var ErrSnapshotUnidentified = errors.New("snapshot has no path or id")

ErrSnapshotUnidentified is returned by the ships that address a snapshot by its Path and ID (archive, restic) when the engine could not resolve them. The snapshot exists; the ship cannot find it.

View Source
var StateDir = func() string { return "/var/lib/bentoo/snapshot" }

StateDir is the directory holding persisted snapshot state. It is a var so tests can redirect it away from the system path.

Functions

func Apply

func Apply(ctx context.Context, cfg *Config, configPath string, run Runner) error

Apply materializes the native config and scheduler for cfg: it renders+writes the engine's native config (driver-aware) and, when a systemd schedule is configured, installs and enables the timer. run is the injectable subprocess seam.

func ArchiveObjectLeaf

func ArchiveObjectLeaf(id string) string

ArchiveObjectLeaf is the object name RELATIVE to its prefix directory: "<id>.zst". This is exactly what `rclone lsjson <remote>/<prefix>` reports in each entry's Name — relative to the LISTED path, carrying no prefix — so a scoped listing of "-home" yields "snap1.zst", not "-home/snap1.zst".

Comparing a listing entry against a FULL key (ArchiveObjectName) therefore matches nothing, and it does so SILENTLY: no error, no empty result, just a comparison that is never true. Where that comparison is a guard, "matches nothing" means "the guard protects nothing". Compare listing entries with this function; use ArchiveObjectName only for keys relative to the remote root.

func ArchiveObjectName

func ArchiveObjectName(subvolume, id string) string

ArchiveObjectName derives the deterministic FULL remote object key — relative to the remote ROOT — for a snapshot of subvolume with the given id: "<ArchivePrefix(subvolume)>/<ArchiveObjectLeaf(id)>", e.g. subvolume "/home" with id "snap1" → "-home/snap1.zst". It is the EXPORTED mirror of the unexported archiveObjectName(Snapshot) used on the ship side, so the restore path can build the object key from a subvolume + id pair without holding a full Snapshot value. Keeping both on the same convention guarantees the restore reads back exactly the key the archive shipper wrote.

This is a full key, NOT a listing entry: see ArchiveObjectLeaf for what `rclone lsjson` reports under a scoped path.

func ArchivePrefix

func ArchivePrefix(subvolume string) string

ArchivePrefix is the remote sub-path that holds one subvolume's objects: the subvolume path put through the same sanitize rule the parent store already uses for its filenames, with NO special case — including for the root subvolume "/", whose prefix is therefore the single directory "-".

The property that makes this prefix unambiguous is that sanitize maps every byte outside [A-Za-z0-9._-] to '-' and so can NEVER emit '/'. The '/' that ArchiveObjectName appends is therefore the only one in the key: a prefix can never bleed into the leaf, and two nested subvolumes can never produce the same key. The old flat scheme joined prefix and id with '-', a byte sanitize CAN emit, so subvolume "/home" with id "otaku-42" and subvolume "/home/otaku" with id "42" both rendered as "-home-otaku-42.zst".

func BtrbkConfPath

func BtrbkConfPath(snapshotConfPath string) string

BtrbkConfPath returns where the rendered btrbk.conf lives for a given snapshot.toml path — the sibling file btrbk.conf in the same directory. An empty snapshotConfPath falls back to DefaultBtrbkConfPath.

func ConfigPaths

func ConfigPaths() ([]string, error)

func DefaultConfigPath

func DefaultConfigPath() (string, error)

DefaultConfigPath returns the highest-priority config path (system scope). It is the write target for `apply`-generated config.

func FindConfigPath

func FindConfigPath() (string, error)

FindConfigPath returns the first existing config path, or the default path when none exists yet (so callers have a stable location to create one).

func InstallEmergeHook

func InstallEmergeHook() error

InstallEmergeHook installs the opt-in Portage emerge hook: it writes the snapper pre/post hook script to /etc/portage/bashrc.d and ensures /etc/portage/bashrc sources it via the managed block. Idempotent: re-running rewrites the script and replaces the existing block in place — user bashrc content outside the block is preserved byte-for-byte and the block is never duplicated.

It is called exclusively by the explicit `bentoo snapshot hook --install` verb — never by apply.

func LastRunPath

func LastRunPath() string

LastRunPath is the canonical path of the most recent RunResult.

func PlanApply

func PlanApply(cfg *Config, configPath string) []string

PlanApply returns the actions `snapshot apply` would perform for cfg as human-readable "would ..." lines: the engine config work it would do (driver-aware, mirroring WriteEngineConfig — a file for btrbk, snapper commands for snapper) and — when a systemd schedule is configured — the unit files it would install plus the systemctl invocations (mirroring systemdScheduler.Apply). Callers validate cfg first; an unknown engine driver contributes no engine line.

func PlanPrune

func PlanPrune(cfg *Config, shipScope string) []string

PlanPrune returns the actions `snapshot prune` would perform for cfg as human-readable "would ..." lines: per subvolume the engine-native prune (driver-aware invocation summary), then one remote GFS line per in-scope archive ship with the retention it would enforce — Manager.Prune's order. A non-empty shipScope mirrors the real --ship scoping: the engine line(s) are omitted and only the named ship's remote is planned. Callers reject an unknown shipScope BEFORE planning (the cmd layer exits 1), so an unmatched scope here simply yields no lines.

func PlanRun

func PlanRun(cfg *Config) []string

PlanRun returns the pipeline `snapshot run` would execute for cfg as human-readable "would ..." lines: per subvolume the engine create+prune, then one ship line per [[ship]] target, in manager.go's Run order.

func ResolveRestoreSubvolume

func ResolveRestoreSubvolume(cfg *Config, flag string) (string, error)

ResolveRestoreSubvolume picks the subvolume a restore reads from, given the engine's configured list and the value of the --subvolume flag ("" when the operator did not pass one):

  • a non-empty flag must name a configured subvolume, else it fails naming the value passed and listing the configured spellings;
  • an empty flag with exactly ONE configured subvolume yields that one, so the single-subvolume deployment keeps working with no new flag;
  • an empty flag with TWO OR MORE is ambiguous and fails, naming every configured subvolume so the operator can retry without opening the config.

The rule lives in the package, not the cobra handler, because restore is DESTRUCTIVE: as a pure function over (config, flag) a unit test pins it branch by branch. Callers resolve BEFORE calling Restore, so an ambiguous request fails ahead of any subprocess. Matching is exact string equality: the configured spelling is the key ArchivePrefix sanitizes, so normalising the flag (trimming a trailing '/', say) would send the restore to the wrong objects.

func Restore

func Restore(ctx context.Context, id, target string, opts RestoreOptions) error

Restore restores snapshot id into target, dispatching by opts.Driver. Because every restore is destructive, it first enforces the confirm gate: unless opts.Yes is set, it asks opts.Confirm (or defaultConfirmFunc) to approve, and returns ErrRestoreDeclined — running NOTHING — if the operator declines. Only then does it dispatch: "archive" replays the validated delta chain, "restic" runs a (optionally granular) `restic restore`; an unknown driver is rejected with ErrInvalidDriver.

func RestoreChainFor

func RestoreChainFor(cfg *Config, ship ShipConfig, id, subvolume string) []chainLink

RestoreChainFor builds the archive object chain the restore CLI replays for id. It is the EXPORTED seam that keeps chain construction INSIDE this package: chainLink is unexported and cannot be built from package main, so the CLI assigns the result straight into RestoreOptions.Chain. A restic ship needs no chain and gets nil.

subvolume is the ALREADY-RESOLVED subvolume (the caller runs ResolveRestoreSubvolume first); this function makes no choice of its own, since deriving it from the FIRST configured subvolume sent multi-subvolume restores to the wrong prefix. For an archive ship it returns a SINGLE full link (ParentID "") keyed ArchiveObjectName(subvolume, id), the key the archive shipper wrote. cfg is unread but kept: a real chain needs the ship list and retention, and churning an exported signature twice costs more than that.

TODO(incremental-chain): reconstruct the full→…→target delta sequence for an incremental id by listing the remote. Until then a delta-only id fails (correctly) at `btrfs receive` because its base is absent.

func Rollback

func Rollback(ctx context.Context, cfg *Config, id string, opts RollbackOptions) error

Rollback rolls the system back to snapshot id via `snapper rollback`. The ORDER is the contract:

  1. ENGINE GUARD: a non-snapper cfg.Engine.Driver is refused with ErrRollbackUnsupported BEFORE the confirm gate — the operator is never prompted to approve an action that cannot run.
  2. CONFIRM GATE: unless opts.Yes, the operator must approve; a decline returns ErrRollbackDeclined and NO subprocess runs.
  3. EXECUTE: `snapper -c root rollback <id>` through opts.Run. A system rollback always targets the canonical "root" snapper config (snapperConfigName("/")) — rolling back is only meaningful for the root filesystem.

func TimerNextRun

func TimerNextRun(ctx context.Context, run Runner) string

TimerNextRun reports the timer's next scheduled run via `systemctl list-timers bentoo-snapshot.timer --no-pager`. Like TimerState it is best-effort: it returns the trimmed data line naming the timer unit, or "" when no such line is available (callers print "unknown").

func TimerState

func TimerState(ctx context.Context, run Runner) string

TimerState reports the systemd timer's enablement via `systemctl is-enabled bentoo-snapshot.timer`. It is best-effort: a non-zero exit (e.g. the timer is disabled or absent) is mapped to the trimmed output rather than a hard error, so `status` always has something to print.

func UninstallEmergeHook

func UninstallEmergeHook() error

UninstallEmergeHook removes the Portage emerge hook: it deletes the hook script and strips the managed block from /etc/portage/bashrc, leaving user content intact. A bashrc left empty (or whitespace-only) is removed. Absent files are a clean no-op so uninstall succeeds on a never-installed system.

func WriteBtrbkConf

func WriteBtrbkConf(cfg *Config, configPath string) error

WriteBtrbkConf renders the btrbk.conf for cfg and writes it next to the snapshot.toml at configPath. Called by `apply` and by `run` so the run is self-contained even if `apply` was never executed.

func WriteEngineConfig

func WriteEngineConfig(ctx context.Context, cfg *Config, configPath string, run Runner) error

WriteEngineConfig materializes the native config for the configured engine driver: "btrbk" renders btrbk.conf next to the snapshot.toml at configPath; "snapper" provisions the per-subvolume configs through snapper's own create-config/set-config (configPath is unused there — snapper's config location is fixed). An unknown driver fails with ErrInvalidDriver.

It takes a context and a Runner because the snapper driver reaches snapper to do the work rather than writing /etc itself — a file write is not observed by a running snapperd. The btrbk driver uses neither.

The snapper driver's warnings go to the logger ctx carries (logging.FromContext); a ctx without one discards them.

Types

type Config

type Config struct {
	Engine   EngineConfig   `toml:"engine"`
	Ship     []ShipConfig   `toml:"ship,omitempty"`
	Notify   NotifyConfig   `toml:"notify,omitempty"`
	Schedule ScheduleConfig `toml:"schedule,omitempty"`
}

Config is the parsed snapshot.toml. The engine produces snapshots, each ship replicates them, notify reports the outcome, and schedule installs the systemd timer that drives `bentoo snapshot run`.

func Load

func Load() (*Config, error)

Load reads and parses the config from the first existing search path.

func LoadFrom

func LoadFrom(path string) (*Config, error)

LoadFrom reads and parses snapshot.toml from a specific path. It does not validate; call Validate after loading.

Parsing stays lenient: an unknown key is ignored exactly as before. The decode runs through a Decoder purely to obtain the MetaData that drives the ONE targeted migration diagnostic below — toml.Unmarshal is itself just this call with the MetaData discarded, so behavior, errors, and leniency are unchanged. This is deliberately not a general strict-decode pass: only the single removed key is reported, so forward-compatible additions to snapshot.toml stay silent.

LoadFrom is LoadFromWith(path, nil): the migration warning goes to stderr as a bare line.

func LoadFromWith

func LoadFromWith(path string, log *slog.Logger) (*Config, error)

LoadFromWith is LoadFrom reporting the migration warning on log, the invocation's logger. A nil log keeps LoadFrom's bare stderr line.

func (*Config) Validate

func (c *Config) Validate() error

Validate checks the config before any side effect. It fails hard with ErrInvalidDriver on an unknown engine.driver, ship.type, or schedule.backend; warns-but-continues on non-fatal issues (empty subvolumes); and verifies each active driver's binary is on PATH via detect.

Order matters: every enum is checked first, so an unknown driver string is reported before — and independently of — any missing-binary detection, and both happen before the command writes any file.

Validate logs nothing: it is ValidateWith(nil), and a nil logger discards.

func (*Config) ValidateWith

func (c *Config) ValidateWith(log *slog.Logger) error

ValidateWith is Validate with its non-fatal warnings sent to log; a nil log discards them. The verdict is the same as Validate's: log changes only where the warnings go.

type EmailConfig

type EmailConfig struct {
	To   []string   `toml:"to,omitempty"`
	From string     `toml:"from,omitempty"`
	SMTP SMTPConfig `toml:"smtp,omitempty"`
}

EmailConfig configures the email driver. A non-empty To activates the driver; From is the sender header. With SMTP.Host unset the message is piped to the local sendmail binary; setting SMTP.Host switches the transport to direct SMTP via stdlib net/smtp.

type Engine

type Engine interface {
	Name() string
	Create(ctx context.Context, subvolume string) (Snapshot, error)
	Prune(ctx context.Context, subvolume string, policy Retention) ([]Snapshot, error)
	List(ctx context.Context, subvolume string) ([]Snapshot, error)
}

Engine is the snapshot engine contract. Drivers (btrbk, snapper) create, prune, and list snapshots for a subvolume. Retention is delegated to the native tool — Prune passes the policy through rather than computing GFS in Go.

type EngineConfig

type EngineConfig struct {
	Driver      string    `toml:"driver"`
	Subvolumes  []string  `toml:"subvolumes,omitempty"`
	SnapshotDir string    `toml:"snapshot_dir,omitempty"`
	Retention   Retention `toml:"retention,omitempty"`
}

EngineConfig selects and configures the snapshot engine driver. Subvolumes are the btrfs subvolumes to snapshot; SnapshotDir is where the engine keeps them.

type HealthchecksConfig

type HealthchecksConfig struct {
	PingURL string `toml:"ping_url,omitempty"`
	Start   bool   `toml:"start,omitempty"`
}

HealthchecksConfig configures the healthchecks.io driver. PingURL is the base check URL (success → base, failure → /fail); Start optionally pings /start before the run.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager wires the configured drivers and runs the snapshot pipeline.

func NewManager

func NewManager(cfg Config, configPath string, run Runner, opts ...ManagerOption) (*Manager, error)

NewManager builds a Manager from cfg using the factories. configPath is the snapshot.toml path; it locates the sibling btrbk.conf the engine drives. run is the injectable subprocess seam (nil → production execRunner); ssh targets are folded into the engine's btrbk.conf so btrbk performs send/receive during Create.

func (*Manager) List

func (m *Manager) List(ctx context.Context) (map[string][]Snapshot, error)

List returns the local snapshots per configured subvolume. A subvolume that errors aborts the listing with that error.

func (*Manager) ListRemote

func (m *Manager) ListRemote(ctx context.Context) []RemoteGroup

ListRemote collects the remote snapshot listings of every capable source in deterministic order — the engine's target backups first, then each ship in config order. A failing source is recorded in its group's Err and does NOT abort the others, keeping `list` lenient and read-only.

func (*Manager) Prune

func (m *Manager) Prune(ctx context.Context, shipScope string) (RunResult, error)

Prune applies the [engine.retention] policy on demand: the engine-native prune per subvolume (btrbk clean / snapper cleanup timeline), then the remote GFS sweep per archive ship. A non-empty shipScope narrows the prune to the named destination ONLY: the engine-local prune is skipped entirely and just that ship's remote is pruned; an unknown scope returns an error before any stage runs. UNLIKE the post-ship best-effort prune inside Send, every failure here is recorded as a failed stage — a user-invoked prune must not hide errors — and, mirroring Run, a failed result yields a non-nil error. A cancelled context short-circuits before the next stage.

func (*Manager) Run

func (m *Manager) Run(ctx context.Context) (RunResult, error)

Run executes the pipeline per subvolume — Create → Prune → Send for each shipper — accumulating a RunResult. A failed stage is recorded and the run continues so other subvolumes/ships are still attempted; a cancelled context short-circuits before the next subvolume. The Notifier hook is invoked exactly once with the final result.

func (*Manager) Subvolumes

func (m *Manager) Subvolumes() []string

Subvolumes exposes the configured subvolumes (for CLI iteration/reporting).

type ManagerOption

type ManagerOption func(*managerOptions)

ManagerOption configures NewManager.

func WithManagerLogger

func WithManagerLogger(l *slog.Logger) ManagerOption

WithManagerLogger hands the Manager the invocation's logger, which it passes to every engine, ship and notifier it builds. Without it they discard their warnings.

type Notifier

type Notifier interface {
	Notify(ctx context.Context, res RunResult) error
}

Notifier reports a completed run. The no-op default, the ntfy, healthchecks, webhook and email drivers are all composed behind multiNotifier by newNotifier.

type NotifyConfig

type NotifyConfig struct {
	On           []string           `toml:"on,omitempty"`
	Ntfy         NtfyConfig         `toml:"ntfy,omitempty"`
	Healthchecks HealthchecksConfig `toml:"healthchecks,omitempty"`
	Webhook      WebhookConfig      `toml:"webhook,omitempty"`
	Email        EmailConfig        `toml:"email,omitempty"`
}

NotifyConfig selects and configures notification backends. On filters which run outcomes notify — a subset of {"success", "failure"}; an empty On means failure only (see shouldNotify). Each sub-table, when populated, activates one driver; the configured drivers fan out behind the Notifier interface.

type NtfyConfig

type NtfyConfig struct {
	URL string `toml:"url,omitempty"`
}

NtfyConfig configures the ntfy driver. URL is the topic URL. The auth token is not a config field: it resolves from BENTOO_NTFY_TOKEN via the secrets chain (env → user file → system file) and, when set, is sent via the Authorization header and is never logged.

type PipeStage

type PipeStage struct {
	Name string
	Args []string
}

PipeStage is one command in the archive pipe: a program name and its argv. It is the unit the pure builder emits and the executor feeds through the Runner.

type Piper

type Piper interface {
	Pipe(ctx context.Context, stages []PipeStage) (stdout []byte, err error)
}

Piper is the optional streaming seam of a Runner. runPipe requires it: a multi-stage pipe such as `btrfs send | zstd | rclone rcat` must not hold any stage's whole output in memory, which a Run-only Runner cannot avoid.

type RemoteGroup

type RemoteGroup struct {
	Label     string
	Snapshots []Snapshot
	Err       error
}

RemoteGroup is one remote source's contribution to `list --remote`. Label names the source for display ("btrbk targets", or the restic ship's name); Err, when non-nil, marks the source as failed — its listing is absent but the other groups are unaffected (lenient, read-only).

type RestoreOptions

type RestoreOptions struct {
	Driver  string      // "archive" | "restic" (resolved upstream from the --ship entry)
	Yes     bool        // --yes: skip the confirm prompt
	Confirm confirmFunc // nil → defaultConfirmFunc
	Run     Runner      // nil → defaultRunner()

	// archive:
	Remote   string // rclone remote+path prefix, e.g. "gdrive:bentoo-backups"
	Compress string // decompressor program; default "zstd" → `zstd -d`
	// Chain is the ordered full→target object chain to replay, RESOLVED UPSTREAM
	// by the CLI/caller. Reconstructing the chain from remote object metadata is
	// future work; Restore validates, orders and refuses-before-receive on this
	// already-resolved chain.
	Chain []chainLink

	// restic:
	Repo, PasswordFile string // non-secret locators: repo URL + password-FILE PATH
	Include            string // optional single file/subdir for a granular restic restore
}

RestoreOptions configures a Restore. Driver selects the path; Yes/Confirm gate the destructive action; Run is the subprocess seam. The remaining fields are split by driver — Remote/Compress/Chain drive the archive replay, Repo/PasswordFile/Include drive the restic restore.

type Retention

type Retention struct {
	Hourly      int    `toml:"hourly,omitempty"`
	Daily       int    `toml:"daily,omitempty"`
	Weekly      int    `toml:"weekly,omitempty"`
	Monthly     int    `toml:"monthly,omitempty"`
	PreserveMin string `toml:"preserve_min,omitempty"`
}

Retention is a GFS-style retention policy delegated to the native engine (mapped to btrbk snapshot_preserve/target_preserve). The counts are how many of each interval to keep; PreserveMin is btrbk's snapshot_preserve_min (e.g. "latest" or "2d") — left as a string because btrbk accepts both a keyword and a duration there.

type RollbackOptions

type RollbackOptions struct {
	Yes     bool        // --yes: skip the confirm prompt
	Confirm confirmFunc // nil → defaultConfirmFunc
	Run     Runner      // nil → defaultRunner()
}

RollbackOptions configures a Rollback. Yes/Confirm gate the destructive action; Run is the subprocess seam. Conventions mirror RestoreOptions: nil Confirm → defaultConfirmFunc, nil Run → defaultRunner().

type RunResult

type RunResult struct {
	StartedAt time.Time     `json:"started_at"`
	Duration  time.Duration `json:"duration"`
	Stages    []StageResult `json:"stages"`
	Err       string        `json:"error,omitempty"`
}

RunResult is the accumulated outcome of a Manager.Run, persisted so `status` can report the last run. A non-empty Err, or any failed stage, marks the run as failed; partial results (stages completed before a failure) are preserved.

func LoadLastRun

func LoadLastRun() (*RunResult, error)

LoadLastRun reads the most recent persisted RunResult.

func LoadRunResult

func LoadRunResult(path string) (*RunResult, error)

LoadRunResult reads and decodes a RunResult from path.

func (*RunResult) AddStage

func (r *RunResult) AddStage(s StageResult)

AddStage appends a stage outcome to the result.

func (*RunResult) Failed

func (r *RunResult) Failed() bool

Failed reports whether the run encountered any error — a top-level Err or any stage with StatusFailed.

func (*RunResult) Save

func (r *RunResult) Save(path string) error

Save writes the result to path as indented JSON, atomically (temp + rename).

func (*RunResult) SaveLastRun

func (r *RunResult) SaveLastRun() error

SaveLastRun persists the result to LastRunPath.

type Runner

type Runner interface {
	Run(ctx context.Context, name string, args []string, stdin []byte) (stdout []byte, err error)
}

Runner is the subprocess seam shared by the engine and shipper drivers. Every external command goes through Run, or through the optional Piper seam for a streamed multi-stage pipe; both bind each process to ctx via exec.CommandContext so a cancelled parent kills the child. stdin is piped on the process's standard input, never placed in argv.

func NewReportingRunner

func NewReportingRunner(r tui.Reporter, id string) Runner

NewReportingRunner returns a production Runner that emits stage/done progress events to r (keyed by id) for every command it runs. A nil reporter is normalized to a no-op.

It has no production caller yet: cmd/bentoo's deps.snapshotRunner is nil in production. It stays because it is the only place that sets execRunner.reporter and execRunner.taskID. execRunner is unexported, so nothing outside the package can build a reporting Runner, and without this constructor both fields stay zero and Run's progress-event path is dead code. Wiring it is one assignment: deps.snapshotRunner already reaches every driver through NewManager, newEngine, newShipper and newScheduler, so NewReportingRunner(rep, id) there turns a snapshot run's subprocesses into stage/done events. TestSnapshotRunnerEmitsStageDone and TestSnapshotRunnerReportsFailure prove that Run emits stage:<id>:<name> before a command and done:<id>:<ok> after it, on success and on failure.

type SMTPConfig

type SMTPConfig struct {
	Host string `toml:"host,omitempty"`
	Port int    `toml:"port,omitempty"`
	User string `toml:"user,omitempty"`
}

SMTPConfig is the optional SMTP transport of the email driver. Host selects SMTP over local sendmail and is joined with Port as host:port. Host, Port and User stay in snapshot.toml because they are configuration, not secrets.

The password is deliberately NOT a field here: it resolves from BENTOO_SMTP_PASSWORD via the secrets chain (env → user file → system file) and is never read from snapshot.toml. PLAIN auth is enabled only when User is set AND that lookup yields a non-empty value; either one missing means the message is sent unauthenticated. The resolved password is never placed in argv, error strings, or logs.

type ScheduleConfig

type ScheduleConfig struct {
	Backend         string `toml:"backend,omitempty"`
	OnCalendar      string `toml:"on_calendar,omitempty"`
	Persistent      *bool  `toml:"persistent,omitempty"`
	RandomizedDelay string `toml:"randomized_delay,omitempty"`
}

ScheduleConfig configures unit generation. Backend selects the scheduler driver ("systemd"; empty = no scheduling). Persistent is tri-state (*bool) so an unset value is distinguishable from an explicit false.

type Scheduler

type Scheduler interface {
	Apply(ctx context.Context, cfg ScheduleConfig) error
	Remove(ctx context.Context) error
}

Scheduler installs and removes the periodic trigger for `bentoo snapshot run`. The systemd driver renders a .service + .timer. Apply is idempotent: re-running reconciles the units without creating duplicates.

type ShipConfig

type ShipConfig struct {
	Name   string `toml:"name,omitempty"`
	Type   string `toml:"type"`
	Target string `toml:"target,omitempty"`

	// restic shipper. Repo is the restic repository URL; PasswordFile is the
	// path to the repo password (kept out of the file itself); Compression
	// maps to restic's --compression; MountStrategy selects how the snapshot
	// is exposed to the restic backup (e.g. bind vs read-only mount).
	Repo          string `toml:"repo,omitempty"`
	PasswordFile  string `toml:"password_file,omitempty"`
	Compression   string `toml:"compression,omitempty"`
	MountStrategy string `toml:"mount_strategy,omitempty"`

	// archive shipper. Remote is the rclone remote target (remote:path); Mode
	// selects full vs incremental archiving; Compress selects the stream codec
	// applied before upload.
	Remote   string `toml:"remote,omitempty"`
	Mode     string `toml:"mode,omitempty"`
	Compress string `toml:"compress,omitempty"`
}

ShipConfig is one replication target ([[ship]] array-of-tables). Type selects the shipper driver; Target is the destination (for ssh: user@host:/path). The remaining fields are driver-specific: the restic group (Repo..MountStrategy) applies when type = "restic", the archive group (Remote..Compress) when type = "archive".

type ShipReport

type ShipReport struct {
	Target      string // replication destination (e.g. user@host:/path)
	Snapshot    string // ID/path of the snapshot shipped
	Delegated   bool   // true when the transfer is performed by the engine (btrbk)
	Note        string // human-readable detail
	Incremental bool   // archive: true when sent with -p (incremental)
}

ShipReport is the outcome of a single Shipper.Send. For the ssh shipper the actual byte transfer is delegated to btrbk during engine Create, so Delegated is true and Send only records which target was (or will be) served.

type Shipper

type Shipper interface {
	Name() string
	Send(ctx context.Context, snap Snapshot) (ShipReport, error)
}

Shipper replicates a snapshot to a remote target. Drivers are selected from ship.type via newShipper.

type Snapshot

type Snapshot struct {
	ID        string
	Subvolume string
	Path      string
	CreatedAt time.Time
	ReadOnly  bool
	ParentID  string // "" = full
}

Snapshot describes a single point-in-time btrfs snapshot. ParentID is empty for a full snapshot and set to the parent's ID for an incremental one.

type StageResult

type StageResult struct {
	Subvolume string        `json:"subvolume"`
	Stage     string        `json:"stage"`
	Target    string        `json:"target,omitempty"` // ship target name, for StageShip
	Status    string        `json:"status"`
	StartedAt time.Time     `json:"started_at"`
	Duration  time.Duration `json:"duration"`
	Err       string        `json:"error,omitempty"`
}

StageResult records the outcome of a single pipeline stage for one subvolume. Err is a string (not error) so the result round-trips through JSON for `status`.

type WebhookConfig

type WebhookConfig struct {
	URL     string            `toml:"url,omitempty"`
	Headers map[string]string `toml:"headers,omitempty"`
}

WebhookConfig configures the generic webhook driver. URL receives a POST with the serialized RunResult; Headers are applied to the request. Header values holding secrets are never logged.

Directories

Path Synopsis
Package snapshottest provides test doubles for package snapshot.
Package snapshottest provides test doubles for package snapshot.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL