Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions cmd/cliutil/cliutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (
"github.com/spf13/cobra"
)

// TableFunc renders the table body; the writer is flushed by the caller.
type TableFunc func(w *tabwriter.Writer)

func CommandContext(cmd *cobra.Command) context.Context {
if cmd != nil && cmd.Context() != nil {
return cmd.Context()
Expand Down Expand Up @@ -52,13 +55,13 @@ func MaybeOutputJSON(cmd *cobra.Command, v any) (bool, error) {
return true, OutputJSON(v)
}

func OutputFormatted(cmd *cobra.Command, data any, tableFn func(w *tabwriter.Writer)) error {
func OutputFormatted(cmd *cobra.Command, data any, tableFn TableFunc) error {
format, _ := cmd.Flags().GetString("format")
return OutputFormattedStr(format, data, tableFn)
}

// OutputFormattedStr renders data as JSON when format=="json", else as a table via tableFn.
func OutputFormattedStr(format string, data any, tableFn func(w *tabwriter.Writer)) error {
func OutputFormattedStr(format string, data any, tableFn TableFunc) error {
if format == "json" {
return OutputJSON(data)
}
Expand Down
10 changes: 4 additions & 6 deletions cmd/core/metastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package core
import (
"context"
"path/filepath"
"slices"
"sync"
"time"

Expand Down Expand Up @@ -111,12 +112,9 @@ func ResolveMetaBackend(conf *config.Config) string {
// LegacyJSONPresent reports whether any json-engine namespace file exists
// under the root — data a fresh sqlite store must never shadow.
func LegacyJSONPresent(conf *config.Config) bool {
for _, ns := range MetaJSONNamespaces(conf) {
if utils.FileExists(ns.FilePath) {
return true
}
}
return false
return slices.ContainsFunc(MetaJSONNamespaces(conf), func(ns metajson.Namespace) bool {
return utils.FileExists(ns.FilePath)
})
}

// MetaStore builds the process-wide meta store once — one store, every
Expand Down
13 changes: 13 additions & 0 deletions cmd/core/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ func EnsureSnapshotNameFree(ctx context.Context, snapBackend snapshot.Snapshot,
if name == "" {
return nil
}
// The name index, not Inspect: a save killed mid-flight leaves a pending record
// still holding the name, which Inspect reports as not-found. Passing that
// preflight means the whole capture is written before the insert rejects it.
if nh, ok := snapBackend.(snapshot.NameHolder); ok {
id, held, err := nh.NameOwner(ctx, name)
if err != nil {
return fmt.Errorf("check snapshot name: %w", err)
}
if held {
return fmt.Errorf("snapshot name %q already exists (held by %s)", name, id)
}
return nil
}
if _, err := snapBackend.Inspect(ctx, name); err == nil {
return fmt.Errorf("snapshot name %q already exists", name)
} else if !errors.Is(err, snapshot.ErrNotFound) {
Expand Down
8 changes: 4 additions & 4 deletions cmd/snapshot/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (h Handler) Save(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
logger := log.WithFunc("cmd.snapshot.save")
logger := log.WithFunc("cmd.snapshot.Save")

vmRef := args[0]
hyper, err := cmdcore.FindHypervisor(ctx, conf, vmRef)
Expand Down Expand Up @@ -136,7 +136,7 @@ func (h Handler) Export(cmd *cobra.Command, args []string) (err error) {
if err != nil {
return err
}
logger := log.WithFunc("cmd.snapshot.export")
logger := log.WithFunc("cmd.snapshot.Export")
snapBackend, err := cmdcore.InitSnapshot(ctx, conf)
if err != nil {
return err
Expand Down Expand Up @@ -222,7 +222,7 @@ func (h Handler) Import(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
logger := log.WithFunc("cmd.snapshot.import")
logger := log.WithFunc("cmd.snapshot.Import")
snapBackend, err := cmdcore.InitSnapshot(ctx, conf)
if err != nil {
return err
Expand Down Expand Up @@ -263,7 +263,7 @@ func (h Handler) RM(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
logger := log.WithFunc("cmd.snapshot.rm")
logger := log.WithFunc("cmd.snapshot.RM")
snapBackend, err := cmdcore.InitSnapshot(ctx, conf)
if err != nil {
return err
Expand Down
5 changes: 1 addition & 4 deletions cmd/storebench/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
// storebench times meta-engine operations through the hypervisor backend:
// `update`/`get` are single-process loops at resident N (P0 paired gate and
// §9 anchors); `create` fans out worker PROCESSES doing reserve+finalize on
// one shared store — the concurrent VM-creation shape (§9).
// storebench times meta-engine operations through the hypervisor backend: update/get loop in-process at resident N, create fans out worker processes doing reserve+finalize on one shared store (§9's concurrent VM-creation shape).
package main

import (
Expand Down
1 change: 0 additions & 1 deletion cmd/vm/reseed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"testing"

"github.com/cocoonstack/cocoon-agent/agent"

"github.com/cocoonstack/cocoon/types"
)

Expand Down
34 changes: 24 additions & 10 deletions cmd/vm/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,12 @@ func (h Handler) Clone(cmd *cobra.Command, args []string) error {
defer stream.Close() //nolint:errcheck
defer cmdcore.CloseOnCancel(ctx, stream)()

vmCfg, vmID, rollbackReserve, unlock, netProvider, netSetup, err := h.prepareClone(ctx, cmd, conf, hyper, cfg)
cs, err := h.prepareClone(ctx, cmd, conf, hyper, cfg)
if err != nil {
return err
}
vmCfg, vmID, rollbackReserve, unlock := cs.vmCfg, cs.vmID, cs.rollback, cs.unlock
netProvider, netSetup := cs.netProvider, cs.netSetup
defer unlock()

logger.Infof(ctx, "cloning VM from snapshot %s ...", snapID)
Expand Down Expand Up @@ -283,10 +285,12 @@ func (h Handler) cloneFromDir(ctx context.Context, cmd *cobra.Command, conf *con
}

func (h Handler) cloneFromSrcDir(ctx context.Context, cmd *cobra.Command, conf *config.Config, hyper hypervisor.Hypervisor, dcr hypervisor.Direct, cfg types.SnapshotConfig, srcDir, sourceLabel string, logger *log.Fields) error {
vmCfg, vmID, rollbackReserve, unlock, netProvider, netSetup, err := h.prepareClone(ctx, cmd, conf, hyper, cfg)
cs, err := h.prepareClone(ctx, cmd, conf, hyper, cfg)
if err != nil {
return err
}
vmCfg, vmID, rollbackReserve, unlock := cs.vmCfg, cs.vmID, cs.rollback, cs.unlock
netProvider, netSetup := cs.netProvider, cs.netSetup
defer unlock()

wantJSON := cliutil.WantJSON(cmd)
Expand All @@ -310,32 +314,42 @@ func (h Handler) cloneFromSrcDir(ctx context.Context, cmd *cobra.Command, conf *
return nil
}

func (h Handler) prepareClone(ctx context.Context, cmd *cobra.Command, conf *config.Config, hyper hypervisor.Hypervisor, cfg types.SnapshotConfig) (*types.VMConfig, string, func(), func(), network.Network, types.NetSetup, error) {
// cloneSetup is prepareClone's result: the reserved clone's identity and network plus the rollback/unlock pair the caller owes until finalize.
type cloneSetup struct {
vmCfg *types.VMConfig
vmID string
rollback func()
unlock func()
netProvider network.Network
netSetup types.NetSetup
}

func (h Handler) prepareClone(ctx context.Context, cmd *cobra.Command, conf *config.Config, hyper hypervisor.Hypervisor, cfg types.SnapshotConfig) (cloneSetup, error) {
vmCfg, err := cmdcore.CloneVMConfigFromFlags(cmd, cfg)
if err != nil {
return nil, "", nil, nil, nil, types.NetSetup{}, err
return cloneSetup{}, err
}
vmID := utils.GenerateID()
if vmCfg.Name == "" {
vmCfg.Name = "cocoon-clone-" + network.VMIDPrefix(vmID)
}
if err = vmCfg.Validate(); err != nil {
return nil, "", nil, nil, nil, types.NetSetup{}, err
return cloneSetup{}, err
}
// Envelope pins share create's digest-lock window; a record-backed clone's source pin already protects these.
releasePins, err := cmdcore.PinEnvelopeBlobs(ctx, conf, cfg.ImageBlobIDs)
if err != nil {
return nil, "", nil, nil, nil, types.NetSetup{}, err
return cloneSetup{}, err
}
rollbackReserve, unlock, err := prereserveVM(ctx, hyper, vmID, vmCfg, cfg.ImageBlobIDs)
releasePins()
if err != nil {
return nil, "", nil, nil, nil, types.NetSetup{}, err
return cloneSetup{}, err
}
fail := func(err error) (*types.VMConfig, string, func(), func(), network.Network, types.NetSetup, error) {
fail := func(err error) (cloneSetup, error) {
rollbackReserve()
unlock()
return nil, "", nil, nil, nil, types.NetSetup{}, err
return cloneSetup{}, err
}

if pull, _ := cmd.Flags().GetBool("pull"); pull && vmCfg.Image != "" && vmCfg.ImageType != "" {
Expand Down Expand Up @@ -363,7 +377,7 @@ func (h Handler) prepareClone(ctx context.Context, cmd *cobra.Command, conf *con
return fail(err)
}

return vmCfg, vmID, rollbackReserve, unlock, netProvider, netSetup, nil
return cloneSetup{vmCfg: vmCfg, vmID: vmID, rollback: rollbackReserve, unlock: unlock, netProvider: netProvider, netSetup: netSetup}, nil
}

func (h Handler) restoreDirect(ctx context.Context, cmd *cobra.Command, conf *config.Config, snapRef, vmRef string, vmCfg *types.VMConfig, snapBackend snapshot.Snapshot, hyper hypervisor.Hypervisor, logger *log.Fields) (bool, error) {
Expand Down
6 changes: 3 additions & 3 deletions docs/gc.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ journalctl -u cocoon-gc.service --since today | awk '/gc.Run completed/'
```

Reasons:
- **snapshot**: `orphan` (dataDir without DB record), `stale-pending` (Create crashed >24h ago), `lru-all` / `lru-age` / `lru-keep` / `lru-size` (multi-criterion uses `+` joiner)
- **cloud-hypervisor / firecracker**: `orphan-runDir`, `orphan-logDir`, `stale-creating`
- **snapshot**: `orphan` (dataDir without DB record), `stale-pending` (a dead save's pending record — its build lease is free), `lru-all` / `lru-age` / `lru-keep` / `lru-size` (multi-criterion uses `+` joiner)
- **cloud-hypervisor / firecracker**: `orphan-runDir`, `orphan-logDir`, `stale-creating` (a dead create/clone's placeholder — its ops lock is free, no age wait)
- **images (oci, cloudimg)**: `unreferenced`
- **cni**: `orphan` (netns without active VM)
- **bridge**: `orphan-tap`
- **vmlock**: `orphan-lease` (lease file for a VM no backend knows)

### Snapshot LRU Eviction

Bare `cocoon gc` only reclaims **orphans** (on-disk data with no DB record) and **stale pending** records (crashed mid-Create, older than 24h). To also evict healthy snapshots by access recency, pass `--snapshot`:
Bare `cocoon gc` only reclaims **orphans** (on-disk data with no DB record) and **stale pending** records (a save died mid-flight; every save holds its snapshot's build lease from placeholder to finalize, so a pending record whose lease GC can acquire is provably ownerless — no age wait). To also evict healthy snapshots by access recency, pass `--snapshot`:

| Flag | Effect |
| -------------------- | ----------------------------------------------------------------------------------------------- |
Expand Down
2 changes: 1 addition & 1 deletion hypervisor/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const (
// COWRawFileName is the raw COW disk's file name in the run dir (single owner so snapshot matchers and clone path rewrites can't drift).
COWRawFileName = "cow.raw"

// CreatingStateGCGrace bounds how long GC tolerates a "creating" VM.
// CreatingStateGCGrace ages leftover capture/staging dirs and clone locks, where no held lock proves the owner died; creating records need no age — their ops lock is the proof.
CreatingStateGCGrace = 24 * time.Hour

// VMMemTransferTimeout is the single-shot timeout for snapshot/restore API calls.
Expand Down
5 changes: 1 addition & 4 deletions hypervisor/choreo_trace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ import (
"github.com/cocoonstack/cocoon/types"
)

// TestLegacyChoreographyTrace is the P0 differential gate (design §10): the
// legacy implementation ran this exact op sequence and recorded returned
// values, error identities and final bytes; the migrated boundary must
// reproduce every step.
// TestLegacyChoreographyTrace is the P0 differential gate (design §10): the migrated boundary must reproduce the legacy run's returned values, error identities and final bytes step for step.
func TestLegacyChoreographyTrace(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "legacy-choreo-trace.json"))
if err != nil {
Expand Down
35 changes: 15 additions & 20 deletions hypervisor/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ import (
// AfterExtractFn finalizes a cloned VM after snapshot files are in place; sourceSnapshotID flows through for metering lineage.
type AfterExtractFn func(ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, runDir, logDir string, now time.Time, sourceSnapshotID string) (*types.VM, error)

// CloneSpec carries one clone's inputs through the shared placeholder→populate→finalize skeleton.
type CloneSpec struct {
VMCfg *types.VMConfig
Net types.NetSetup
SnapshotConfig *types.SnapshotConfig
AfterExtract AfterExtractFn
}

// DirectCloneBase clones from a local snapshot directory. Used when the snapshot lives on the same host (no tar streaming needed).
func (b *Backend) DirectCloneBase(
ctx context.Context, vmID string, vmCfg *types.VMConfig,
net types.NetSetup, snapshotConfig *types.SnapshotConfig, srcDir string,
cloneFiles func(dstDir, srcDir string) error,
afterExtract AfterExtractFn,
) (*types.VM, error) {
return b.cloneBase(ctx, vmID, vmCfg, net, snapshotConfig, afterExtract, func(runDir string) error {
func (b *Backend) DirectCloneBase(ctx context.Context, vmID string, spec CloneSpec, srcDir string, cloneFiles func(dstDir, srcDir string) error) (*types.VM, error) {
return b.cloneBase(ctx, vmID, spec, func(runDir string) error {
if err := cloneFiles(runDir, srcDir); err != nil {
return fmt.Errorf("clone snapshot files: %w", err)
}
Expand All @@ -30,12 +33,8 @@ func (b *Backend) DirectCloneBase(
}

// CloneFromStream clones from a tar stream into a fresh runDir. Used when the snapshot arrives over the network (cross-node clone).
func (b *Backend) CloneFromStream(
ctx context.Context, vmID string, vmCfg *types.VMConfig,
net types.NetSetup, snapshotConfig *types.SnapshotConfig, snapshot io.Reader,
afterExtract AfterExtractFn,
) (*types.VM, error) {
return b.cloneBase(ctx, vmID, vmCfg, net, snapshotConfig, afterExtract, func(runDir string) error {
func (b *Backend) CloneFromStream(ctx context.Context, vmID string, spec CloneSpec, snapshot io.Reader) (*types.VM, error) {
return b.cloneBase(ctx, vmID, spec, func(runDir string) error {
if err := utils.ExtractTar(runDir, snapshot, isLockFile); err != nil {
return fmt.Errorf("extract snapshot: %w", err)
}
Expand All @@ -62,12 +61,8 @@ func (b *Backend) FinalizeClone(ctx context.Context, vmID string, info *types.VM
return nil
}

func (b *Backend) cloneBase(
ctx context.Context, vmID string, vmCfg *types.VMConfig,
net types.NetSetup, snapshotConfig *types.SnapshotConfig,
afterExtract AfterExtractFn, populate func(runDir string) error,
) (_ *types.VM, err error) {
runDir, logDir, now, cleanup, err := b.reservePlaceholder(ctx, vmID, vmCfg, snapshotConfig.ImageBlobIDs)
func (b *Backend) cloneBase(ctx context.Context, vmID string, spec CloneSpec, populate func(runDir string) error) (_ *types.VM, err error) {
runDir, logDir, now, cleanup, err := b.reservePlaceholder(ctx, vmID, spec.VMCfg, spec.SnapshotConfig.ImageBlobIDs)
if err != nil {
return nil, err
}
Expand All @@ -79,5 +74,5 @@ func (b *Backend) cloneBase(
if err = populate(runDir); err != nil {
return nil, err
}
return afterExtract(ctx, vmID, vmCfg, net, runDir, logDir, now, snapshotConfig.ID)
return spec.AfterExtract(ctx, vmID, spec.VMCfg, spec.Net, runDir, logDir, now, spec.SnapshotConfig.ID)
}
11 changes: 3 additions & 8 deletions hypervisor/cloudhypervisor/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type cloneResumeOpts struct {
}

func (ch *CloudHypervisor) Clone(ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, snapshotConfig *types.SnapshotConfig, snapshot io.Reader) (*types.VM, error) {
return ch.CloneFromStream(ctx, vmID, vmCfg, net, snapshotConfig, snapshot, ch.cloneAfterExtract)
return ch.CloneFromStream(ctx, vmID, hypervisor.CloneSpec{VMCfg: vmCfg, Net: net, SnapshotConfig: snapshotConfig, AfterExtract: ch.cloneAfterExtract}, snapshot)
}

func (ch *CloudHypervisor) cloneAfterExtract(ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, runDir, logDir string, now time.Time, sourceSnapshotID string) (*types.VM, error) {
Expand All @@ -45,7 +45,7 @@ func (ch *CloudHypervisor) cloneAfterExtract(ctx context.Context, vmID string, v
// cloneAfterExtractParsed is cloneAfterExtract minus the config.json parse; DirectClone passes the copy step's parse of the verbatim-copied source.
func (ch *CloudHypervisor) cloneAfterExtractParsed(ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, runDir, logDir string, now time.Time, sourceSnapshotID string, chCfg *chVMConfig) (*types.VM, error) {
networkConfigs := net.NetworkConfigs
logger := log.WithFunc("cloudhypervisor.cloneAfterExtract")
logger := log.WithFunc("cloudhypervisor.cloneAfterExtractParsed")

chConfigPath := filepath.Join(runDir, configJSONName)
vmCfg.RestoreMode = resolveRestoreMode(ctx, vmCfg.RestoreMode, chCfg.Memory)
Expand Down Expand Up @@ -158,12 +158,7 @@ func (ch *CloudHypervisor) cloneAfterExtractParsed(ctx context.Context, vmID str
return info, nil
}

func (ch *CloudHypervisor) restoreAndResumeClone(
ctx context.Context,
pid int,
sockPath, runDir string,
opts *cloneResumeOpts,
) (err error) {
func (ch *CloudHypervisor) restoreAndResumeClone(ctx context.Context, pid int, sockPath, runDir string, opts *cloneResumeOpts) (err error) {
defer func() {
if err != nil {
ch.AbortLaunch(ctx, pid, sockPath, runDir, runtimeFiles)
Expand Down
10 changes: 7 additions & 3 deletions hypervisor/cloudhypervisor/direct.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ import (
func (ch *CloudHypervisor) DirectClone(ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, snapshotConfig *types.SnapshotConfig, srcDir string) (*types.VM, error) {
// The copy step's parse feeds cloneAfterExtractParsed: config.json is copied verbatim, so re-parsing the runDir copy would decode identical bytes.
var srcCfg *chVMConfig
return ch.DirectCloneBase(ctx, vmID, vmCfg, net, snapshotConfig, srcDir, func(dstDir, srcDir string) error {
spec := hypervisor.CloneSpec{
VMCfg: vmCfg, Net: net, SnapshotConfig: snapshotConfig,
AfterExtract: func(ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, runDir, logDir string, now time.Time, sourceSnapshotID string) (*types.VM, error) {
return ch.cloneAfterExtractParsed(ctx, vmID, vmCfg, net, runDir, logDir, now, sourceSnapshotID, srcCfg)
},
}
return ch.DirectCloneBase(ctx, vmID, spec, srcDir, func(dstDir, srcDir string) error {
var err error
srcCfg, err = cloneSnapshotFiles(ctx, dstDir, srcDir)
return err
}, func(ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, runDir, logDir string, now time.Time, sourceSnapshotID string) (*types.VM, error) {
return ch.cloneAfterExtractParsed(ctx, vmID, vmCfg, net, runDir, logDir, now, sourceSnapshotID, srcCfg)
})
}

Expand Down
Loading