Skip to content

feat: add shellctl home snapshot save/restore endpoints - #40975

Closed
GareArc wants to merge 16 commits into
mainfrom
feat/shellctl-snapshot-endpoints
Closed

feat: add shellctl home snapshot save/restore endpoints#40975
GareArc wants to merge 16 commits into
mainfrom
feat/shellctl-snapshot-endpoints

Conversation

@GareArc

@GareArc GareArc commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds POST /v1/snapshot/save and POST /v1/snapshot/restore to shellctl.

  • save — chunked stream, 204 when Home is empty, integrity trailers (X-Snapshot-Status / X-Snapshot-Sha256 / X-Snapshot-Bytes). A mid-stream failure aborts the connection, so a truncated stream can never be read as success.
  • restore — extraction under os.Root (openat2/RESOLVE_BENEATH on Linux), 400 archive_malformed vs 500 restore_failed, setuid/setgid/sticky stripped, ownership never applied.
  • 409 snapshot_busy single-flight across both endpoints; I/O deadlines bound how long a stalled peer can hold the gate.

Excludes are configurable via SHELLCTL_HOME_SNAPSHOT_EXCLUDES (default workspace).

Notes for review

  • Adds github.com/klauspost/compress — the module's first compression dependency.
  • Neither endpoint imposes a size limit, by design: callers own size policy. zstd amplification means bounding a compressed restore stream does not bound the bytes written to disk, so a cap here would be false assurance.
  • The os.Root kernel path is untested on darwin; worth a linux/amd64 CI run.

GareArc added 13 commits August 19, 2026 02:10
An empty Home now streams an ordinary 16-byte archive with no entries
instead of a 204. Callers no longer need a branch for it: the snapshot is
stored, resolved, and restored through the same path as any other, and a
consumer cannot mistake an absent snapshot for an empty one.
Workspace content is not logically part of a Home Snapshot, per the agent
backend's HomeSnapshotBackend protocol. Enforcing that through the default
value of SHELLCTL_HOME_SNAPSHOT_EXCLUDES let any deployment silently opt out
of it by setting the variable to something else.

SaveHome now always skips the top-level workspace directory. The env var
becomes a purely additive list of further excludes and defaults to empty.
@GareArc
GareArc force-pushed the feat/shellctl-snapshot-endpoints branch 2 times, most recently from 8728f41 to 259cda8 Compare August 19, 2026 23:13
SnapshotTimeout was hardcoded to 600s, so tuning it for slow networks,
large Home directories, or loaded nodes meant rebuilding the image.
SHELLCTL_SNAPSHOT_TIMEOUT now overrides it with a Go duration string.

An unparseable or non-positive value fails startup rather than falling
back to the default. A non-positive deadline is the dangerous case: it
expires before the first write, and since the deadline is what releases
the single-operation gate when a peer stalls, every save and restore
would return 409 from then on.

Both snapshot env vars are parsed and checked in DefaultConfig, which
returns an error, so an unusable value yields no config at all and the
caller has a single gate. That absorbs Config.Validate, a one-line
pass-through to snapshot.ValidateExcludes, and ValidateExcludes itself:
exported for this one consumer and called by nothing in its own package,
its rule is about the shape of an env value, not about snapshotting. A
malformed exclude is already inert in excluded(), which only matches
top-level names.
@GareArc
GareArc force-pushed the feat/shellctl-snapshot-endpoints branch from 259cda8 to c68ed66 Compare August 19, 2026 23:22
GareArc and others added 2 commits August 19, 2026 16:24
Removed comments explaining the parseHomeSnapshotExcludes function.
This deadline is the runtime defending its own liveness, not a budget for
the operation. It exists because the save/restore gate is held for the
whole call: if the caller stops reading, the write blocks, the gate is
never released, and every later save and restore on that sandbox answers
409 snapshot_busy until the deadline fires. At 600s that wedge lasted
nearly ten minutes.

45s puts it just above the 30s its caller now allows, which is the
ordering that matters. Above, and the caller aborts first and reports its
own timeout while this gate frees shortly after. Equal or below, and the
runtime aborts first, the caller reads a stream that ends without an ok
trailer, and a slow snapshot is reported as a corrupt archive.

SHELLCTL_SNAPSHOT_TIMEOUT still overrides it; only the shipped default
moves.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant