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
52 changes: 20 additions & 32 deletions cmd/core/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,19 @@ import (
)

var hypervisorFactories = []hypervisorFactory{
{config.HypervisorCH, func(c *config.Config) (hypervisor.Hypervisor, error) { return cloudhypervisor.New(c) }},
{config.HypervisorFirecracker, func(c *config.Config) (hypervisor.Hypervisor, error) { return firecracker.New(c) }},
{config.HypervisorCH, func(ctx context.Context, c *config.Config) (hypervisor.Hypervisor, error) {
return cloudhypervisor.New(c, MeteringRecorder(ctx, c))
}},
{config.HypervisorFirecracker, func(ctx context.Context, c *config.Config) (hypervisor.Hypervisor, error) {
return firecracker.New(c, MeteringRecorder(ctx, c))
}},
}

// hypervisorFactory keeps backend lookup and iteration order together.
type hypervisorFactory struct {
typ config.HypervisorType
ctor func(*config.Config) (hypervisor.Hypervisor, error)
ctor func(context.Context, *config.Config) (hypervisor.Hypervisor, error)
}

// BaseHandler provides shared config access for all command handlers.
type BaseHandler struct {
ConfProvider func() *config.Config
}
Expand Down Expand Up @@ -71,7 +73,6 @@ func (h BaseHandler) Conf() (*config.Config, error) {
return conf, nil
}

// CommandContext returns cmd.Context() or Background (test-only fallback).
func CommandContext(cmd *cobra.Command) context.Context {
if cmd != nil && cmd.Context() != nil {
return cmd.Context()
Expand All @@ -84,7 +85,7 @@ func InitBackends(ctx context.Context, conf *config.Config) ([]imagebackend.Imag
if err != nil {
return nil, nil, err
}
hyper, err := InitHypervisor(conf)
hyper, err := InitHypervisor(ctx, conf)
if err != nil {
return nil, nil, err
}
Expand All @@ -111,22 +112,22 @@ func InitImageBackendsForPull(ctx context.Context, conf *config.Config) (*oci.OC
return ociStore, cloudimgStore, nil
}

func InitHypervisor(conf *config.Config) (hypervisor.Hypervisor, error) {
func InitHypervisor(ctx context.Context, conf *config.Config) (hypervisor.Hypervisor, error) {
ctor := findHypervisorFactory(conf.Hypervisor())
if ctor == nil {
return nil, fmt.Errorf("unknown hypervisor type: %s", conf.Hypervisor())
}
h, err := ctor(conf)
h, err := ctor(ctx, conf)
if err != nil {
return nil, fmt.Errorf("init hypervisor: %w", err)
}
return h, nil
}

func InitAllHypervisors(conf *config.Config) ([]hypervisor.Hypervisor, error) {
func InitAllHypervisors(ctx context.Context, conf *config.Config) ([]hypervisor.Hypervisor, error) {
result := make([]hypervisor.Hypervisor, 0, len(hypervisorFactories))
for _, f := range hypervisorFactories {
h, err := f.ctor(conf)
h, err := f.ctor(ctx, conf)
if err != nil {
return nil, fmt.Errorf("init %s for GC: %w", f.typ, err)
}
Expand All @@ -136,7 +137,7 @@ func InitAllHypervisors(conf *config.Config) ([]hypervisor.Hypervisor, error) {
}

func FindHypervisor(ctx context.Context, conf *config.Config, ref string) (hypervisor.Hypervisor, error) {
hypers, err := InitAllHypervisors(conf)
hypers, err := InitAllHypervisors(ctx, conf)
if err != nil {
return nil, err
}
Expand All @@ -156,7 +157,7 @@ func ListAllVMs(ctx context.Context, hypers []hypervisor.Hypervisor) ([]*types.V
return all, nil
}

// RouteRefs resolves user refs to (hypervisor → full VM IDs); downstream callers never re-resolve.
// RouteRefs resolves user refs to (hypervisor → full VM IDs).
func RouteRefs(ctx context.Context, hypers []hypervisor.Hypervisor, refs []string) (map[hypervisor.Hypervisor][]string, error) {
result := map[hypervisor.Hypervisor][]string{}
for _, ref := range refs {
Expand Down Expand Up @@ -185,8 +186,8 @@ func InitBridgeNetwork(conf *config.Config, bridgeDev string) (network.Network,
return p, nil
}

func InitSnapshot(conf *config.Config, opts ...localfile.Option) (snapshot.Snapshot, error) {
s, err := localfile.New(conf, opts...)
func InitSnapshot(ctx context.Context, conf *config.Config, opts ...localfile.Option) (snapshot.Snapshot, error) {
s, err := localfile.New(conf, MeteringRecorder(ctx, conf), opts...)
if err != nil {
return nil, fmt.Errorf("init snapshot backend: %w", err)
}
Expand Down Expand Up @@ -334,7 +335,6 @@ func VMConfigFromFlags(cmd *cobra.Command, image string) (*types.VMConfig, error
return cfg, nil
}

// CloneVMConfigFromFlags builds VMConfig for clone (inherits from snapshot).
func CloneVMConfigFromFlags(cmd *cobra.Command, snapCfg types.SnapshotConfig) (*types.VMConfig, error) {
vmName, _ := cmd.Flags().GetString("name")
flagNetwork, _ := cmd.Flags().GetString("network")
Expand All @@ -350,7 +350,6 @@ func CloneVMConfigFromFlags(cmd *cobra.Command, snapCfg types.SnapshotConfig) (*

onDemand, _ := cmd.Flags().GetBool("on-demand")

// Validate runs in prepareClone, after the default name is filled in.
return &types.VMConfig{
Name: vmName,
Config: types.Config{
Expand Down Expand Up @@ -385,7 +384,6 @@ func RestoreVMConfigFromFlags(cmd *cobra.Command, vm *types.VM, snapCfg types.Sn
Name: vm.Config.Name,
OnDemand: onDemand,
}
// Guard against tampered --from-dir --force envelopes.
if err := result.Validate(); err != nil {
return nil, fmt.Errorf("snapshot config: %w", err)
}
Expand All @@ -398,7 +396,6 @@ func EnsureFirmwarePath(conf *config.Config, bootCfg *types.BootConfig) {
}
}

// ReconcileState detects stale running records via process liveness.
func ReconcileState(vm *types.VM) string {
if vm.State == types.VMStateRunning && !utils.IsProcessAlive(vm.PID) {
return "stopped (stale)"
Expand All @@ -416,26 +413,23 @@ func AddFormatFlag(cmd *cobra.Command) {
cmd.Flags().StringP("format", "o", "table", `output format: "table" or "json"`)
}

// AddOutputFlag adds --output/-o for lifecycle commands. Empty default keeps the human-readable log output; "json" emits a parseable result on stdout.
func AddOutputFlag(cmd *cobra.Command) {
cmd.Flags().StringP("output", "o", "", `emit "json" for machine-readable output`)
}

// WantJSON reports whether --output=json was requested.
func WantJSON(cmd *cobra.Command) bool {
out, _ := cmd.Flags().GetString("output")
return out == "json"
}

// MaybeOutputJSON emits JSON iff --output=json; (true, _) means handled and the caller should stop logging.
// MaybeOutputJSON emits JSON iff --output=json; (true, _) means caller should stop logging.
func MaybeOutputJSON(cmd *cobra.Command, v any) (bool, error) {
if !WantJSON(cmd) {
return false, nil
}
return true, OutputJSON(v)
}

// OutputFormatted outputs as JSON or table based on --format flag.
func OutputFormatted(cmd *cobra.Command, data any, tableFn func(w *tabwriter.Writer)) error {
format, _ := cmd.Flags().GetString("format")
if format == "json" {
Expand Down Expand Up @@ -509,7 +503,7 @@ func digestPullRef(image, digest, imageType string) string {
return ref.Context().String() + "@" + digest
}

func findHypervisorFactory(typ config.HypervisorType) func(*config.Config) (hypervisor.Hypervisor, error) {
func findHypervisorFactory(typ config.HypervisorType) func(context.Context, *config.Config) (hypervisor.Hypervisor, error) {
for _, f := range hypervisorFactories {
if f.typ == typ {
return f.ctor
Expand Down Expand Up @@ -538,11 +532,9 @@ func resolveVMOwner(ctx context.Context, hypers []hypervisor.Hypervisor, ref str
return owner, resolved, err
}

// sanitizeVMName derives a safe VM name from an image reference.
func sanitizeVMName(image string) string {
ref, err := name.ParseReference(image)
if err != nil {
// Unparseable — fall back to simple replace.
n := strings.ReplaceAll(image, "/", "-")
n = strings.ReplaceAll(n, ":", "-")
n = "cocoon-" + n
Expand All @@ -552,14 +544,10 @@ func sanitizeVMName(image string) string {
return n
}

// RepositoryStr() strips the registry hostname.
// Docker Hub official images get "library/" prepended — strip it.
repo := ref.Context().RepositoryStr()
repo = strings.TrimPrefix(repo, "library/")

repo := strings.TrimPrefix(ref.Context().RepositoryStr(), "library/")
n := "cocoon-" + strings.ReplaceAll(repo, "/", "-")

// Append tag (but not digest — it's too long and not human-readable).
// Skip digest (too long); use tag if not latest.
if tag, ok := ref.(name.Tag); ok && tag.TagStr() != "latest" {
n += "-" + tag.TagStr()
}
Expand Down
37 changes: 37 additions & 0 deletions cmd/core/metering.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package core

import (
"context"
"os"
"path/filepath"
"sync"

"github.com/projecteru2/core/log"

"github.com/cocoonstack/cocoon/config"
"github.com/cocoonstack/cocoon/metering"
)

const (
meteringSubdir = "metering"
meteringFile = "ledger.jsonl"
)

var (
meteringOnce sync.Once
meteringRec metering.Recorder
)

// MeteringRecorder returns a process-wide lifecycle recorder; lazy-init shares one ledger fd across all backends, falls back to NopRecorder on fs error.
func MeteringRecorder(ctx context.Context, conf *config.Config) metering.Recorder {
meteringOnce.Do(func() {
dir := filepath.Join(conf.RootDir, meteringSubdir)
if err := os.MkdirAll(dir, 0o750); err != nil {
log.WithFunc("core.MeteringRecorder").Warnf(ctx, "mkdir %s: %v; metering disabled", dir, err)
meteringRec = metering.NopRecorder{}
return
}
meteringRec = metering.NewFileRecorder(ctx, filepath.Join(dir, meteringFile))
})
return meteringRec
}
2 changes: 0 additions & 2 deletions cmd/images/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
cmdcore "github.com/cocoonstack/cocoon/cmd/core"
)

// Actions defines image management operations.
type Actions interface {
Pull(cmd *cobra.Command, args []string) error
Import(cmd *cobra.Command, args []string) error
Expand All @@ -15,7 +14,6 @@ type Actions interface {
Inspect(cmd *cobra.Command, args []string) error
}

// Command builds the "image" parent command with all subcommands.
func Command(h Actions) *cobra.Command {
imageCmd := &cobra.Command{
Use: "image",
Expand Down
2 changes: 0 additions & 2 deletions cmd/others/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ import (
"github.com/spf13/cobra"
)

// Actions defines cross-cutting system operations.
type Actions interface {
GC(cmd *cobra.Command, args []string) error
Version(cmd *cobra.Command, args []string) error
}

// Commands builds system command set (gc, version, completion).
func Commands(h Actions) []*cobra.Command {
gcCmd := &cobra.Command{
Use: "gc",
Expand Down
4 changes: 2 additions & 2 deletions cmd/others/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (h Handler) GC(cmd *cobra.Command, _ []string) error {
if err != nil {
return err
}
snapBackend, err := cmdcore.InitSnapshot(conf, localfile.WithGCPolicy(policy))
snapBackend, err := cmdcore.InitSnapshot(ctx, conf, localfile.WithGCPolicy(policy))
if err != nil {
return err
}
Expand All @@ -45,7 +45,7 @@ func (h Handler) GC(cmd *cobra.Command, _ []string) error {
b.RegisterGC(o)
}
// Register ALL hypervisor backends so GC protects blobs from both CH and FC VMs.
hypers, hyperErr := cmdcore.InitAllHypervisors(conf)
hypers, hyperErr := cmdcore.InitAllHypervisors(ctx, conf)
if hyperErr != nil {
return hyperErr
}
Expand Down
2 changes: 0 additions & 2 deletions cmd/snapshot/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
cmdcore "github.com/cocoonstack/cocoon/cmd/core"
)

// Actions defines snapshot management operations.
type Actions interface {
Save(cmd *cobra.Command, args []string) error
List(cmd *cobra.Command, args []string) error
Expand All @@ -16,7 +15,6 @@ type Actions interface {
Import(cmd *cobra.Command, args []string) error
}

// Command builds the "snapshot" parent command with all subcommands.
func Command(h Actions) *cobra.Command {
snapshotCmd := &cobra.Command{
Use: "snapshot",
Expand Down
20 changes: 7 additions & 13 deletions cmd/snapshot/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,13 @@ func (h Handler) Save(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("find VM %s: %w", vmRef, err)
}
snapBackend, err := cmdcore.InitSnapshot(conf)
snapBackend, err := cmdcore.InitSnapshot(ctx, conf)
if err != nil {
return err
}
name, _ := cmd.Flags().GetString("name")
description, _ := cmd.Flags().GetString("description")

// Pre-check: reject if the snapshot name is already taken.
if name != "" {
if _, inspectErr := snapBackend.Inspect(ctx, name); inspectErr == nil {
return fmt.Errorf("snapshot name %q already exists", name)
Expand All @@ -59,8 +58,7 @@ func (h Handler) Save(cmd *cobra.Command, args []string) error {
}
defer stream.Close() //nolint:errcheck

// Close stream on context cancellation to unblock the pipe immediately,
// so Ctrl+C doesn't hang while streaming large snapshot data.
// Close stream on ctx cancel so Ctrl+C doesn't hang on the pipe.
stop := context.AfterFunc(ctx, func() {
stream.Close() //nolint:errcheck,gosec
})
Expand All @@ -85,12 +83,11 @@ func (h Handler) List(cmd *cobra.Command, _ []string) error {
if err != nil {
return err
}
snapBackend, err := cmdcore.InitSnapshot(conf)
snapBackend, err := cmdcore.InitSnapshot(ctx, conf)
if err != nil {
return err
}

// Optional: filter by VM ownership.
vmRef, _ := cmd.Flags().GetString("vm")
var filterIDs map[string]struct{}
if vmRef != "" {
Expand All @@ -114,7 +111,6 @@ func (h Handler) List(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("list: %w", err)
}

// Apply VM filter if specified.
if filterIDs != nil {
filtered := snapshots[:0]
for _, s := range snapshots {
Expand Down Expand Up @@ -148,7 +144,7 @@ func (h Handler) Inspect(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
snapBackend, err := cmdcore.InitSnapshot(conf)
snapBackend, err := cmdcore.InitSnapshot(ctx, conf)
if err != nil {
return err
}
Expand All @@ -166,7 +162,7 @@ func (h Handler) Export(cmd *cobra.Command, args []string) (err error) {
return err
}
logger := log.WithFunc("cmd.snapshot.export")
snapBackend, err := cmdcore.InitSnapshot(conf)
snapBackend, err := cmdcore.InitSnapshot(ctx, conf)
if err != nil {
return err
}
Expand Down Expand Up @@ -209,15 +205,13 @@ func (h Handler) Export(cmd *cobra.Command, args []string) (err error) {
})
defer stop()

// Stream to stdout when output is "-".
if output == "-" {
if _, err = io.Copy(os.Stdout, stream); err != nil {
return fmt.Errorf("write archive: %w", err)
}
return nil
}

// Derive default output filename from snapshot name or ID.
if output == "" {
snap, inspectErr := snapBackend.Inspect(ctx, ref)
if inspectErr != nil {
Expand Down Expand Up @@ -258,7 +252,7 @@ func (h Handler) Import(cmd *cobra.Command, args []string) error {
return err
}
logger := log.WithFunc("cmd.snapshot.import")
snapBackend, err := cmdcore.InitSnapshot(conf)
snapBackend, err := cmdcore.InitSnapshot(ctx, conf)
if err != nil {
return err
}
Expand Down Expand Up @@ -295,7 +289,7 @@ func (h Handler) RM(cmd *cobra.Command, args []string) error {
return err
}
logger := log.WithFunc("cmd.snapshot.rm")
snapBackend, err := cmdcore.InitSnapshot(conf)
snapBackend, err := cmdcore.InitSnapshot(ctx, conf)
if err != nil {
return err
}
Expand Down
Loading
Loading