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
1 change: 1 addition & 0 deletions cmd/snapshot/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/cocoonstack/cocoon/types"
)

// Handler groups the snapshot subcommands.
type Handler struct {
cmdcore.BaseHandler
}
Expand Down
1 change: 1 addition & 0 deletions gc/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Orchestrator struct {
modules []runner
}

// New returns an Orchestrator with no registered modules.
func New() *Orchestrator { return &Orchestrator{} }

// Register is package-level because Go methods can't have type params.
Expand Down
8 changes: 0 additions & 8 deletions hypervisor/cloudhypervisor/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,6 @@ func TestPatchCHConfig_DiskCountMismatch(t *testing.T) {
}
}

// updateCOWPath

func TestUpdateCOWPath_OCI(t *testing.T) {
configs := []*types.StorageConfig{
{Path: "/old/layer.erofs", RO: true, Serial: "layer0", Role: types.StorageRoleLayer},
Expand Down Expand Up @@ -210,8 +208,6 @@ func TestUpdateCOWPath_Cloudimg(t *testing.T) {
}
}

// rebuildBootConfig

func TestRebuildBootConfig(t *testing.T) {
t.Run("nil_payload", func(t *testing.T) {
cfg := &chVMConfig{}
Expand Down Expand Up @@ -258,8 +254,6 @@ func TestRebuildBootConfig(t *testing.T) {
})
}

// patchStateJSON

func TestPatchStateJSON(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "state.json")
Expand Down Expand Up @@ -628,5 +622,3 @@ func basePatchOpts() *patchOptions {
directBoot: true,
}
}

// patchCHConfig
1 change: 1 addition & 0 deletions hypervisor/cloudhypervisor/direct.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func (ch *CloudHypervisor) DirectClone(ctx context.Context, vmID string, vmCfg *
return ch.DirectCloneBase(ctx, vmID, vmCfg, net, snapshotConfig, srcDir, cloneSnapshotFiles, ch.cloneAfterExtract)
}

// DirectRestore restores a VM in place from a local snapshot dir.
func (ch *CloudHypervisor) DirectRestore(ctx context.Context, vmRef string, vmCfg *types.VMConfig, srcDir, sourceSnapshotID string) (*types.VM, error) {
return ch.DirectRestoreSequence(ctx, vmRef, hypervisor.DirectRestoreSpec{
VMCfg: vmCfg,
Expand Down
6 changes: 3 additions & 3 deletions hypervisor/cloudhypervisor/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ import (
"github.com/cocoonstack/cocoon/utils"
)

// chMemoryRestoreMode controls how CH restores guest memory from a snapshot.
type chMemoryRestoreMode string

const (
pidFileName = "ch.pid"
cmdlineFileName = "cmdline"
Expand All @@ -41,6 +38,9 @@ const (

var runtimeFiles = []string{hypervisor.APISocketName, pidFileName, hypervisor.ConsoleSockName, cmdlineFileName, hypervisor.VsockSockName}

// chMemoryRestoreMode controls how CH restores guest memory from a snapshot.
type chMemoryRestoreMode string

type chRestoreConfig struct {
SourceURL string `json:"source_url"`
MemoryRestoreMode chMemoryRestoreMode `json:"memory_restore_mode,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions hypervisor/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func (b *Backend) ReserveVM(ctx context.Context, id string, vmCfg *types.VMConfi
})
}

// RollbackCreate removes the placeholder record and its name mapping after a failed create.
func (b *Backend) RollbackCreate(ctx context.Context, id, name string) {
if err := b.DB.Update(ctx, func(idx *VMIndex) error {
delete(idx.VMs, id)
Expand Down
1 change: 1 addition & 0 deletions hypervisor/firecracker/direct.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func (fc *Firecracker) DirectClone(ctx context.Context, vmID string, vmCfg *type
return fc.DirectCloneBase(ctx, vmID, vmCfg, net, snapshotConfig, srcDir, cloneSnapshotFiles, fc.cloneAfterExtract)
}

// DirectRestore restores a VM in place from a local snapshot dir.
func (fc *Firecracker) DirectRestore(ctx context.Context, vmRef string, vmCfg *types.VMConfig, srcDir, sourceSnapshotID string) (*types.VM, error) {
return fc.DirectRestoreSequence(ctx, vmRef, hypervisor.DirectRestoreSpec{
VMCfg: vmCfg,
Expand Down
1 change: 1 addition & 0 deletions hypervisor/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
// SnapshotMetaFile is the cocoon-owned sidecar carrying fields the hypervisor's native config can't hold (Role/MountPoint/FSType/DirectIO; FC CPU/Memory).
const SnapshotMetaFile = "cocoon.json"

// SnapshotMeta is the schema of the cocoon.json snapshot sidecar.
type SnapshotMeta struct {
StorageConfigs []*types.StorageConfig `json:"storage_configs"`
BootConfig *types.BootConfig `json:"boot_config,omitempty"`
Expand Down
14 changes: 7 additions & 7 deletions images/cloudimg/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,6 @@ type imageIndex struct {
images.Index[imageEntry]
}

type imageEntry struct {
Ref string `json:"ref"`
ContentSum images.Digest `json:"content_sum"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"created_at"`
}

// Lookup finds an entry by URL or content digest.
func (idx *imageIndex) Lookup(id string) (string, *imageEntry, bool) {
if entry, ok := idx.Images[id]; ok && entry != nil {
Expand All @@ -34,6 +27,13 @@ func (idx *imageIndex) LookupRefs(id string) []string {
return images.LookupRefs(idx.Images, id)
}

type imageEntry struct {
Ref string `json:"ref"`
ContentSum images.Digest `json:"content_sum"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"created_at"`
}

func (e imageEntry) EntryID() string { return e.ContentSum.String() }
func (e imageEntry) EntryRef() string { return e.Ref }
func (e imageEntry) EntryCreatedAt() time.Time { return e.CreatedAt }
Expand Down
30 changes: 15 additions & 15 deletions images/oci/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,6 @@ type imageIndex struct {
images.Index[imageEntry]
}

// Paths derive from digests at runtime; not stored.
type imageEntry struct {
Ref string `json:"ref"`
ManifestDigest images.Digest `json:"manifest_digest"`
Layers []layerEntry `json:"layers"`
KernelLayer images.Digest `json:"kernel_layer"`
InitrdLayer images.Digest `json:"initrd_layer"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"created_at"`
}

type layerEntry struct {
Digest images.Digest `json:"digest"`
}

// Lookup finds an entry by ref (exact or normalized) or manifest digest.
func (idx *imageIndex) Lookup(id string) (string, *imageEntry, bool) {
if entry, ok := idx.Images[id]; ok && entry != nil {
Expand Down Expand Up @@ -56,6 +41,17 @@ func (idx *imageIndex) LookupRefs(id string) []string {
})
}

// Paths derive from digests at runtime; not stored.
type imageEntry struct {
Ref string `json:"ref"`
ManifestDigest images.Digest `json:"manifest_digest"`
Layers []layerEntry `json:"layers"`
KernelLayer images.Digest `json:"kernel_layer"`
InitrdLayer images.Digest `json:"initrd_layer"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"created_at"`
}

func (e imageEntry) EntryID() string { return e.ManifestDigest.String() }
func (e imageEntry) EntryRef() string { return e.Ref }
func (e imageEntry) EntryCreatedAt() time.Time { return e.CreatedAt }
Expand All @@ -67,3 +63,7 @@ func (e imageEntry) DigestHexes() []string {
}
return hexes
}

type layerEntry struct {
Digest images.Digest `json:"digest"`
}
1 change: 1 addition & 0 deletions images/op.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func (ops Ops[I, E]) Inspect(ctx context.Context, id string) (result *types.Imag
return result, err
}

// List returns every image in the index.
func (ops Ops[I, E]) List(ctx context.Context) (result []*types.Image, err error) {
err = ops.Store.With(ctx, func(idx *I) error {
result = listImages(ops.Entries(idx), ops.Type, ops.Sizer)
Expand Down
1 change: 1 addition & 0 deletions lock/flock/flock.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Lock struct {
fl *flock.Flock // active flock fd, non-nil while held
}

// New returns a Lock guarding path; the lock file is created on first acquire.
func New(path string) *Lock {
return &Lock{path: path, ch: make(chan struct{}, 1)}
}
Expand Down
2 changes: 2 additions & 0 deletions metering/capture/capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import (

var _ metering.Recorder = (*Recorder)(nil)

// Recorder collects emitted entries in memory for test assertions.
type Recorder struct {
mu sync.Mutex
entries []metering.Entry
}

// New returns an empty in-memory Recorder.
func New() *Recorder { return &Recorder{} }

func (r *Recorder) Emit(_ context.Context, e metering.Entry) {
Expand Down
1 change: 1 addition & 0 deletions metering/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Recorder struct {
f *os.File
}

// New opens or creates the append-only ledger at path.
func New(path string) (*Recorder, error) {
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) //nolint:gosec
if err != nil {
Expand Down
9 changes: 4 additions & 5 deletions metering/metering.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ const (
ReasonSnapRemove Reason = "snap-rm"
)

var (
_ io.WriterTo = Entry{}
_ Recorder = NopRecorder{}
)

// Kind identifies a lifecycle endpoint; downstream pairs *.start with *.stop by id.
type Kind string

Expand All @@ -45,6 +40,8 @@ type Shape struct {
StorageBytes int64 `json:"storage_bytes,omitempty"`
}

var _ io.WriterTo = Entry{}

// Entry is one append-only lifecycle event.
type Entry struct {
Kind Kind `json:"kind"`
Expand Down Expand Up @@ -73,6 +70,8 @@ type Recorder interface {
Emit(context.Context, Entry)
}

var _ Recorder = NopRecorder{}

// NopRecorder discards every entry; zero value is usable.
type NopRecorder struct{}

Expand Down
1 change: 1 addition & 0 deletions metering/stderr/stderr.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Recorder struct {
out io.Writer
}

// New returns a Recorder that writes entries to os.Stderr.
func New() *Recorder { return &Recorder{out: os.Stderr} }

func (r *Recorder) Emit(_ context.Context, e metering.Entry) {
Expand Down
1 change: 1 addition & 0 deletions snapshot/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/cocoonstack/cocoon/utils"
)

// ErrNotFound is returned when a snapshot ref matches no record.
var ErrNotFound = errors.New("snapshot not found")

// SnapshotRecord is the persisted record for a single snapshot.
Expand Down
2 changes: 2 additions & 0 deletions storage/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import (

var _ storage.Store[struct{}] = (*Store[struct{}])(nil)

// Store is a lock-guarded JSON file holding one value of type T.
type Store[T any] struct {
filePath string
locker lock.Locker
}

// New returns a Store backed by filePath and guarded by locker.
func New[T any](filePath string, locker lock.Locker) *Store[T] {
return &Store[T]{filePath: filePath, locker: locker}
}
Expand Down
64 changes: 32 additions & 32 deletions types/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,38 @@ type VMConfig struct {
DataDisks []DataDiskSpec `json:"-"` // populated from --data-disk; consumed by Create
}

// Validate checks that VMConfig fields are within acceptable ranges.
func (cfg *VMConfig) Validate() error {
if cfg.Name == "" {
return fmt.Errorf("vm name cannot be empty")
}
if !validName.MatchString(cfg.Name) {
return fmt.Errorf("vm name %q is invalid: must match %s (max 63 chars)", cfg.Name, validName.String())
}
if cfg.CPU <= 0 {
return fmt.Errorf("--cpu must be at least 1, got %d", cfg.CPU)
}
if cfg.Memory < 512<<20 {
return fmt.Errorf("--memory must be at least 512M, got %d", cfg.Memory)
}
if cfg.Storage < 10<<30 {
return fmt.Errorf("--storage must be at least 10G, got %d", cfg.Storage)
}
if cfg.QueueSize < 0 {
return fmt.Errorf("--queue-size must be non-negative, got %d", cfg.QueueSize)
}
if cfg.DiskQueueSize < 0 {
return fmt.Errorf("--disk-queue-size must be non-negative, got %d", cfg.DiskQueueSize)
}
if cfg.User != "" && !validUsername.MatchString(cfg.User) {
return fmt.Errorf("--user %q is invalid: must be a lowercase Linux username (letters, digits, underscores, hyphens)", cfg.User)
}
if cfg.Password != "" && shellUnsafe.MatchString(cfg.Password) {
return fmt.Errorf("--password contains unsafe shell or YAML characters")
}
return nil
}

// NetSetup is the VM's host networking state, embedded in VM and reused as the initNetwork → hypervisor handoff.
type NetSetup struct {
NetBackend string `json:"net_backend,omitempty"`
Expand Down Expand Up @@ -72,38 +104,6 @@ type VM struct {
StoppedAt *time.Time `json:"stopped_at,omitempty"`
}

// Validate checks that VMConfig fields are within acceptable ranges.
func (cfg *VMConfig) Validate() error {
if cfg.Name == "" {
return fmt.Errorf("vm name cannot be empty")
}
if !validName.MatchString(cfg.Name) {
return fmt.Errorf("vm name %q is invalid: must match %s (max 63 chars)", cfg.Name, validName.String())
}
if cfg.CPU <= 0 {
return fmt.Errorf("--cpu must be at least 1, got %d", cfg.CPU)
}
if cfg.Memory < 512<<20 {
return fmt.Errorf("--memory must be at least 512M, got %d", cfg.Memory)
}
if cfg.Storage < 10<<30 {
return fmt.Errorf("--storage must be at least 10G, got %d", cfg.Storage)
}
if cfg.QueueSize < 0 {
return fmt.Errorf("--queue-size must be non-negative, got %d", cfg.QueueSize)
}
if cfg.DiskQueueSize < 0 {
return fmt.Errorf("--disk-queue-size must be non-negative, got %d", cfg.DiskQueueSize)
}
if cfg.User != "" && !validUsername.MatchString(cfg.User) {
return fmt.Errorf("--user %q is invalid: must be a lowercase Linux username (letters, digits, underscores, hyphens)", cfg.User)
}
if cfg.Password != "" && shellUnsafe.MatchString(cfg.Password) {
return fmt.Errorf("--password contains unsafe shell or YAML characters")
}
return nil
}

// ResolvedNetnsPath returns NetnsPath, with NIC[0] fallback.
func (v *VM) ResolvedNetnsPath() string {
if v == nil {
Expand Down
Loading