From 523cc775f92ccf9512cda049980c7e2566c9de99 Mon Sep 17 00:00:00 2001 From: CMGS Date: Sun, 27 Sep 2026 12:38:01 +0800 Subject: [PATCH 1/2] cut: require cocoon v0.6.9 and drop the empty-store banner guard from the snapshot list cocoon v0.6.9 prints [] for an empty snapshot list --format json, so the prose-banner guard in SnapshotList is dead once the floor moves. RequiredCocoon and docs/deploy.md move to v0.6.9; a non-JSON reply is now a parse error the reconcile logs. --- docs/deploy.md | 11 ++++++----- sandboxd/engine/engine.go | 9 ++------- sandboxd/engine/engine_test.go | 19 +++++++++++++++++++ sandboxd/engine/version_test.go | 10 +++++----- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/docs/deploy.md b/docs/deploy.md index 308dd438..f383dcde 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -6,13 +6,14 @@ the cocoon CLI and needs a template image with silkd baked in. ## Prerequisites - Linux with KVM (`/dev/kvm`) -- [cocoon](https://github.com/cocoonstack/cocoon) **v0.5.2 or newer** installed - and working (`cocoon vm run` boots a Cloud Hypervisor VM). v0.5.2 includes +- [cocoon](https://github.com/cocoonstack/cocoon) **v0.6.9 or newer** installed + and working (`cocoon vm run` boots a Cloud Hypervisor VM). v0.5.2 brought the disk hot-attach used by read-only volumes and the parallel-clone and snapshot/store performance work the - [performance](performance.md) numbers assume. sandboxd logs a warning at - startup when the detected cocoon is below v0.5.2 (a dev/`master-` build - is assumed current) + [performance](performance.md) numbers assume; v0.6.9 prints `[]` for an + empty `snapshot list --format json`, which sandboxd's snapshot reconcile + reads. sandboxd logs a warning at startup when the detected cocoon is below + v0.6.9 (a dev/`master-` build is assumed current) - The sandbox boot artifact installed where cocoon finds it (`/boot/vmlinuz-sandbox`, `/boot/initrd.img-sandbox` — from `ghcr.io/cocoonstack/sandbox/boot:`) diff --git a/sandboxd/engine/engine.go b/sandboxd/engine/engine.go index a7494412..7b112e10 100644 --- a/sandboxd/engine/engine.go +++ b/sandboxd/engine/engine.go @@ -25,8 +25,8 @@ import ( ) const ( - // RequiredCocoon carries the snapshot/store performance baseline. - RequiredCocoon = "v0.5.2" + // RequiredCocoon carries the snapshot/store baseline and the JSON form of an empty snapshot list. + RequiredCocoon = "v0.6.9" StaleCreateCollected StaleCreateOutcome = "collected" StaleCreateBusy StaleCreateOutcome = "busy" @@ -178,11 +178,6 @@ func (e *Engine) SnapshotList(ctx context.Context) ([]string, error) { if err != nil { return nil, err } - // an empty store prints a human line ("No snapshots found."), not JSON. - out = bytes.TrimSpace(out) - if len(out) == 0 || out[0] != '[' { - return nil, nil - } var snaps []struct { Name string `json:"name"` } diff --git a/sandboxd/engine/engine_test.go b/sandboxd/engine/engine_test.go index 7143625a..c1d6100c 100644 --- a/sandboxd/engine/engine_test.go +++ b/sandboxd/engine/engine_test.go @@ -122,6 +122,25 @@ func TestDialGuestPortCtxCancel(t *testing.T) { } } +func TestSnapshotListReadsAnEmptyStoreAndRejectsProse(t *testing.T) { + dir := t.TempDir() + script := "#!/bin/sh\nif [ \"$COCOON_TEST_PROSE\" = 1 ]; then echo 'No snapshots found.'; else echo '[]'; fi\n" + if err := os.WriteFile(filepath.Join(dir, "cocoon"), []byte(script), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + e := New("cocoon", nil, nil, false, false, "") + + t.Setenv("COCOON_TEST_PROSE", "0") + if snaps, err := e.SnapshotList(t.Context()); err != nil || len(snaps) != 0 { + t.Fatalf("empty store: snaps=%v err=%v, want none and nil", snaps, err) + } + t.Setenv("COCOON_TEST_PROSE", "1") + if _, err := e.SnapshotList(t.Context()); err == nil || !strings.Contains(err.Error(), "parse snapshot list") { + t.Fatalf("prose banner: err=%v, want a parse error", err) + } +} + func TestProbeTimeout(t *testing.T) { path := sockPath(t) diff --git a/sandboxd/engine/version_test.go b/sandboxd/engine/version_test.go index e85b8bf2..b71b03bd 100644 --- a/sandboxd/engine/version_test.go +++ b/sandboxd/engine/version_test.go @@ -8,11 +8,11 @@ func TestBelowFloor(t *testing.T) { below bool comparable bool }{ - {"v0.5.2", false, true}, - {"v0.5.3", false, true}, - {"v0.6.0", false, true}, - {"v0.5.1", true, true}, - {"v0.5.0", true, true}, + {"v0.6.9", false, true}, + {"v0.6.10", false, true}, + {"v0.7.0", false, true}, + {"v0.6.8", true, true}, + {"v0.5.2", true, true}, {"v0.4.9", true, true}, {"v1.0.0", false, true}, {"v0.10.0", false, true}, From 6dc0e79e77bae0fe01b62f915ef3c1e4043067c9 Mon Sep 17 00:00:00 2001 From: CMGS Date: Sun, 27 Sep 2026 12:38:24 +0800 Subject: [PATCH 2/2] fix: findVM lists VMs without a positional name argument cocoon vm list has been cobra.NoArgs since v0.6.6 (#237), so the name findVM passed as a positional argument made every call fail with 'unknown command'. vsockOf after a create, tapOf on the egress lane, the resident-memory stat and confirmGone all degraded on it; confirmGone recorded a removed VM as survived and the release answered 500. findVM now lists everything and keeps its own name filter; List loses the variadic so no caller can bring the argument back. --- e2e/fakeengine_test.go | 5 +---- sandboxd/engine/engine.go | 7 +++---- sandboxd/engine/engine_test.go | 13 +++++++++++++ sandboxd/pool/pool.go | 2 +- sandboxd/pool/pool_test.go | 5 +---- sandboxd/pool/refill.go | 2 +- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/e2e/fakeengine_test.go b/e2e/fakeengine_test.go index 42d21a42..a5e81f7a 100644 --- a/e2e/fakeengine_test.go +++ b/e2e/fakeengine_test.go @@ -91,14 +91,11 @@ func (f *fakeEngine) Restore(_ context.Context, name, _ string) (string, error) return f.create(name) } -func (f *fakeEngine) List(_ context.Context, filters ...string) ([]types.VMRecord, error) { +func (f *fakeEngine) List(_ context.Context) ([]types.VMRecord, error) { f.mu.Lock() defer f.mu.Unlock() var vms []types.VMRecord for name, sock := range f.socks { - if len(filters) > 0 && !slices.Contains(filters, name) { - continue - } vms = append(vms, types.VMRecord{State: "running", VsockSocket: sock, Config: types.VMConfig{Name: name}}) } return vms, nil diff --git a/sandboxd/engine/engine.go b/sandboxd/engine/engine.go index 7b112e10..5e46c25a 100644 --- a/sandboxd/engine/engine.go +++ b/sandboxd/engine/engine.go @@ -193,10 +193,9 @@ func (e *Engine) SnapshotList(ctx context.Context) ([]string, error) { return names, nil } -// List returns cocoon's view of local VMs, optionally filtered by name. -func (e *Engine) List(ctx context.Context, filters ...string) ([]types.VMRecord, error) { - args := append([]string{"vm", "list", "--format", formatJSON}, filters...) - out, err := e.run(ctx, args...) +// List returns cocoon's view of local VMs. +func (e *Engine) List(ctx context.Context) ([]types.VMRecord, error) { + out, err := e.run(ctx, "vm", "list", "--format", formatJSON) if err != nil { return nil, err } diff --git a/sandboxd/engine/engine_test.go b/sandboxd/engine/engine_test.go index c1d6100c..ccf939e2 100644 --- a/sandboxd/engine/engine_test.go +++ b/sandboxd/engine/engine_test.go @@ -141,6 +141,19 @@ func TestSnapshotListReadsAnEmptyStoreAndRejectsProse(t *testing.T) { } } +func TestListPassesNoPositionalArgsToCocoon(t *testing.T) { + dir := t.TempDir() + script := "#!/bin/sh\n[ \"$#\" = 4 ] || { echo \"unknown command $4 for cocoon vm list\" >&2; exit 1; }\necho '[{\"config\":{\"name\":\"sbx-1\"},\"state\":\"running\"}]'\n" + if err := os.WriteFile(filepath.Join(dir, "cocoon"), []byte(script), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + vms, err := New("cocoon", nil, nil, false, false, "").List(t.Context()) + if err != nil || len(vms) != 1 || vms[0].Config.Name != "sbx-1" { + t.Fatalf("List = %+v, %v; want the one VM cocoon printed", vms, err) + } +} + func TestProbeTimeout(t *testing.T) { path := sockPath(t) diff --git a/sandboxd/pool/pool.go b/sandboxd/pool/pool.go index 97eed437..0dec2f74 100644 --- a/sandboxd/pool/pool.go +++ b/sandboxd/pool/pool.go @@ -112,7 +112,7 @@ type Engine interface { SnapshotList(ctx context.Context) ([]string, error) Hibernate(ctx context.Context, vmName, snapName string) error Restore(ctx context.Context, vmName, snapRef string) (string, error) - List(ctx context.Context, filters ...string) ([]types.VMRecord, error) + List(ctx context.Context) ([]types.VMRecord, error) Probe(ctx context.Context, vsockSocket string, timeout time.Duration) error DialGuestPort(ctx context.Context, vsockSocket string, port uint16) (net.Conn, error) InstallCACert(ctx context.Context, vsockSocket string, certPEM []byte) error diff --git a/sandboxd/pool/pool_test.go b/sandboxd/pool/pool_test.go index 80b17997..b26d95eb 100644 --- a/sandboxd/pool/pool_test.go +++ b/sandboxd/pool/pool_test.go @@ -1230,15 +1230,12 @@ func (f *fakeEngine) Restore(_ context.Context, name, _ string) (string, error) return f.lateVsock(f.vms[name]), nil } -func (f *fakeEngine) List(_ context.Context, filters ...string) ([]types.VMRecord, error) { +func (f *fakeEngine) List(_ context.Context) ([]types.VMRecord, error) { f.mu.Lock() defer f.mu.Unlock() f.listCount++ var vms []types.VMRecord for name, sock := range f.vms { - if len(filters) > 0 && !slices.Contains(filters, name) { - continue - } sock = f.lateVsock(sock) state := vmStateRunning switch { diff --git a/sandboxd/pool/refill.go b/sandboxd/pool/refill.go index 5373b717..b7adf4e7 100644 --- a/sandboxd/pool/refill.go +++ b/sandboxd/pool/refill.go @@ -448,7 +448,7 @@ func (m *Manager) vsockOf(ctx context.Context, name string) (string, error) { } func (m *Manager) findVM(ctx context.Context, name string) (types.VMRecord, bool, error) { - vms, err := m.eng.List(ctx, name) + vms, err := m.eng.List(ctx) if err != nil { return types.VMRecord{}, false, err }