Skip to content

support clone and restore from specific dir#22

Merged
CMGS merged 15 commits intomasterfrom
feat/clone-from-dir
Apr 30, 2026
Merged

support clone and restore from specific dir#22
CMGS merged 15 commits intomasterfrom
feat/clone-from-dir

Conversation

@CMGS
Copy link
Copy Markdown
Contributor

@CMGS CMGS commented Apr 30, 2026

No description provided.

CMGS added 9 commits April 30, 2026 14:29
Move the canonical snapshot.json envelope read/write into the snapshot
package so cmd/vm can consume an envelope-bearing directory directly
(needed for the upcoming \`vm clone --from-dir\` and
\`vm restore --from-dir\` paths) without depending on localfile internals.

- snapshot.SnapshotJSONName + envelopeVersion (single const block).
- snapshot.ReadSnapshotEnvelope(dir) parses + version-checks; returns
  ErrEnvelopeMissing when the file is absent so callers can surface a
  precise message instead of a generic open error.
- snapshot.WriteSnapshotEnvelope(dir, cfg) writes via AtomicWriteJSON.
- localfile/export.go now references snapshot.SnapshotJSONName.
- localfile/import.go readAndRemoveSnapshotJSON delegates the parse to
  the shared helper; remove-after-read semantics preserved.
Adds the snapshot.DirectoryExporter optional interface and a
LocalFile.ExportToDir implementation. The dir form pairs with the
upcoming \`vm clone --from-dir\` and \`vm restore --from-dir\`,
giving users an rsync-friendly handoff path without a tar round-trip.

- snapshot.DirectoryExporter: ExportToDir(ctx, ref, dir) error
- LocalFile.ExportToDir: rejects non-empty target dirs to avoid silently
  merging into an unrelated tree, writes a fresh snapshot.json envelope
  via WriteSnapshotEnvelope, then ReflinkCopy's each data file. Reflink
  is zero-cost on btrfs/xfs and falls back to plain copy elsewhere; the
  result is always standalone (rsync-friendly), unlike the hardlinks
  DirectClone uses internally for memory pages.
- cmd/snapshot: --to-dir flag, mutually exclusive with --output/--gzip.
Lets users clone from any directory containing a snapshot.json envelope
(e.g. an extracted export.tar, an rsync'd \`snapshot export --to-dir\`
result, or an NFS-mounted golden image) without first registering the
snapshot in the local DB.

- cloneCmd args relaxed to MaximumNArgs(1); positional SNAPSHOT and
  --from-dir are mutually exclusive (validated in the handler so the
  error message names both forms).
- cloneFromDir reads the envelope, routes to the matching backend by
  cfg.Hypervisor, asserts hypervisor.Direct, then funnels through the
  same DirectClone path as the snapshot-DB form.
- cloneDirect refactored to share cloneFromSrcDir with cloneFromDir so
  the prepareClone → DirectClone → finalize sequence isn't duplicated.

The dir is read-only across the call so multiple clones of the same dir
(golden image use case) are safe. Trust boundary unchanged: existing
ValidateMetaPaths still rejects envelopes whose storage paths escape
the local rootDir/runDir.
Symmetric with \`vm clone --from-dir\` but with an extra safety check:
the envelope's snapshot ID must belong to vm.SnapshotIDs unless --force.
This keeps the typical cross-host "sync the same VM's backup over here"
flow zero-friction while making "use unrelated state to overwrite this
VM" an explicit, opt-in operation (data-loss footgun otherwise).

- restoreCmd args relaxed to RangeArgs(1, 2); positional SNAPSHOT and
  --from-dir are mutually exclusive (handler-validated).
- restoreFromDir reads the envelope, resolves the VM's hypervisor,
  asserts hypervisor.Direct, runs the same NIC-count + resource-flag
  validation as the snapshot-DB form, then DirectRestore over the dir.
- --force does NOT add the envelope's ID to vm.SnapshotIDs — restoring
  doesn't change ownership semantics, the foreign snapshot remains
  external to this VM's lineage.
Bullet point in features + extended export flag table + a worked example
covering the three flows: golden image clone, cross-host same-VM
restore, force-overriding a foreign-lineage restore.
Comments per SKILL.md should explain WHY, not narrate or restate the
signature. This pass keeps the load-bearing rationale (atomic write
reason, soft-gate semantics, reflink-vs-hardlink trade-off) and
collapses the rest.
ExportToDir wrote snapshot.json before copying payload files. A
concurrent \`vm restore --from-dir\` reading the envelope could pass
preflight (which only stat()s presence, not byte-completeness) and kill
the running VM while a memory-range or COW file was still mid-copy,
leaving the source VM in Error state with a partial restore.

Move WriteSnapshotEnvelope to after the copy loop so envelope existence
is now an all-data-ready marker. ensureEmptyDir already guarantees the
dir starts empty, so a stale envelope can't survive into the new
export.
Returning *types.SnapshotConfig forced an awkward
\`return &envelope.Config\` and a \`*cfg\` deref inside Write.
SnapshotConfig is small (no identity, embedded Config + a few scalars +
a map ref) so copying is cheap; switching to value drops the pointer
dance in the helper bodies. Callers that need a pointer downstream now
take \`&cfg\` once at the call site, which is symmetric with how every
other local SnapshotConfig in cocoon is handled.

readAndRemoveSnapshotJSON in snapshot/localfile/import.go follows the
same shift; the Import flow now embeds cfg by value into the
SnapshotRecord literal instead of dereferencing.
snapshot.Direct.DataDir, snapshotRecordToConfig, CloneVMConfigFromFlags,
RestoreVMConfigFromFlags, mergeResourceFlags, prepareClone,
cloneFromSrcDir, validateFCCloneOverrides — all of these previously
took or returned *types.SnapshotConfig but only ever read fields. The
chain forced \`return &cfg\` / \`*cfg\` / \`&cfg\` dances at every layer.

Switch to value semantics inside the cmd-layer + snapshot-backend
boundary; the hypervisor backend interface (Clone / DirectClone) keeps
*SnapshotConfig because that's the wider stable API surface, and the
single deref happens at that boundary. SnapshotConfig is small (no
identity, embedded Config + a few scalars + a map ref) so the copy is
cheap and pointer-only mattered for sharing semantics that nothing
actually used here.
@CMGS CMGS force-pushed the feat/clone-from-dir branch from 5f468d3 to 88d82bd Compare April 30, 2026 07:11
CMGS added 6 commits April 30, 2026 15:14
Mirrors the DataDir flip: the snap-DB-side Restore was the only place
in the cmd/vm clone path that still forced \`*cfg\`/\`&cfg\` dancing.
Drop the pointer return on the interface, value-out from LocalFile, and
the cmd-side caller derefs once at the hypervisor boundary just like
DirectClone.
Clone and Restore had near-identical mutex / required-arg blocks; the
only difference was the leading-positional count (0 for clone where
SNAPSHOT is the only arg, 1 for restore where args[0] is VM). Capture
that as baseArgs and let the helper return (fromDir, snapRef) with
exactly one non-empty.
Single caller (the new ExportToDir); the helper added a function-call
indirection without earning it. Inline the 8-line check; the comment
above the block carries the rationale (no silent merge into an unrelated
tree).
- ExportToDir: replace inline mkdir + 3-state switch with utils.EnsureDirs
  (existing 0o750 helper); drop the dead snapshot.json skip in the copy
  loop since registered DataDir results never carry the envelope (Create
  doesn't write it; Import removes it via readAndRemoveSnapshotJSON).
- run.go: 'nic count mismatch' -> 'NIC count mismatch' (ALL-CAPS acronym
  rule); fixes both restoreFromDir and the pre-existing legacy Restore
  message for consistency.
- cloneFromDir: copy *config.Config locally before flipping
  UseFirecracker so the mutation doesn't leak to the caller's shared
  conf (CLI tolerates it; daemons embedding cocoon would notice).
- restoreCmd: PreRunE rejects --force without --from-dir so misuse fails
  loud instead of silently no-op.
…reFromDir

restoreDirect (snapshot-DB path) and restoreFromDir (--from-dir path)
duplicated the wantJSON-log + DirectRestore + output tail. Pull it out
into a single helper, parameterized by sourceLabel. -7 lines net.
… check

Two strict-walk findings:

1. snapshot/localfile/export.go:80 had a literal Version: 1 that should
   reference the snapshot package's version constant. Promote
   envelopeVersion -> EnvelopeVersion (exported) and use
   snapshot.EnvelopeVersion at the cross-package call site. Future
   format bumps now have one source of truth.

2. localfile.LocalFile didn't carry a compile-time assertion against
   snapshot.DirectoryExporter (the new interface). Add it alongside the
   existing Snapshot/Direct/CompressedExporter checks so signature drift
   in either direction surfaces at build time.
@CMGS CMGS changed the title Feat/clone from dir support clone and restore from specific dir Apr 30, 2026
@CMGS CMGS merged commit ac727a5 into master Apr 30, 2026
4 checks passed
@CMGS CMGS deleted the feat/clone-from-dir branch April 30, 2026 09:07
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