Introduce hypervisor.Backend abstraction layer · Issue #1 · stacklok/go-microvm · GitHub
Skip to content

Introduce hypervisor.Backend abstraction layer #1

Description

@JAORMX

Motivation

propolis is currently tightly coupled to libkrun. The orchestration pipeline in propolis.go:Run() directly calls buildKrunConfig() to write .krun_config.json and uses runner.Spawner to launch the propolis-runner subprocess — both libkrun-specific operations.

To support alternative hypervisor backends (notably Hyper-V for Windows), we need an abstraction layer that decouples the shared pipeline (OCI pull, rootfs extraction, hooks, networking, state) from the backend-specific operations (init config format, VM creation, lifecycle management).

This is a pure refactor — no behavioral changes. The libkrun backend wraps existing code.

Proposed Interface

hypervisor.Backend

package hypervisor

type Backend interface {
    // Name returns the backend identifier ("libkrun", "hyperv").
    Name() string

    // PrepareRootFS is called after rootfs hooks, before VM boot.
    // The backend writes init config files (e.g. .krun_config.json for libkrun)
    // or converts the rootfs to a disk image (e.g. VHD for Hyper-V).
    // Returns path to the prepared rootfs (may differ from input if converted).
    PrepareRootFS(ctx context.Context, rootfsPath string, initCfg InitConfig) (string, error)

    // Start boots the VM and returns a handle for lifecycle management.
    Start(ctx context.Context, cfg VMConfig) (VMHandle, error)
}

hypervisor.VMHandle

type VMHandle interface {
    Stop(ctx context.Context) error
    IsAlive() bool
    ID() string // PID as string for libkrun, GUID for Hyper-V
}

Supporting types

type VMConfig struct {
    Name             string
    RootFSPath       string            // Output of PrepareRootFS
    NumVCPUs         uint32
    RAMMiB           uint32
    PortForwards     []PortForward
    FilesystemMounts []FilesystemMount
    InitConfig       InitConfig
    DataDir          string
    ConsoleLogPath   string
    NetEndpoint      NetEndpoint
}

type InitConfig struct {
    Cmd        []string
    Env        []string
    WorkingDir string
}

type NetEndpoint struct {
    Type NetEndpointType // UnixSocket, NamedPipe, HVSocket
    Path string
}

type PortForward struct {
    Host  uint16
    Guest uint16
}

type FilesystemMount struct {
    Tag      string
    HostPath string
}

Why this shape

Two methods on Backend cleanly map to the two backend-specific steps in the current pipeline:

Pipeline step Current code New code
Step 4: Init config buildKrunConfig() + krunCfg.WriteTo() backend.PrepareRootFS(ctx, rootfsPath, initCfg)
Step 6: Boot VM cfg.spawner.Spawn(ctx, runnerCfg) backend.Start(ctx, vmCfg)

Everything else (preflight, image pull, hooks, networking, state) stays shared and unchanged.

Changes required

propolis.go

Replace steps 4 and 6 in Run():

// Step 4: was buildKrunConfig + WriteTo
initCfg := buildInitConfig(rootfs.Config, cfg)
preparedPath, err := backend.PrepareRootFS(ctx, rootfs.Path, initCfg)

// Step 6: was cfg.spawner.Spawn(ctx, runnerCfg)
handle, err := backend.Start(ctx, vmCfg)

options.go

  • Add WithBackend(b hypervisor.Backend) Option
  • Deprecate WithSpawner, WithLibDir, WithRunnerPath (kept for one release cycle, forwarded to libkrun backend config)
  • Platform default: libkrun on Linux/macOS (via build tags)

vm.go

  • Change proc runner.ProcessHandlehandle hypervisor.VMHandle
  • Add ID() string method
  • Keep PID() int as deprecated wrapper (parses ID() as int)

libkrun backend: hypervisor/libkrun/

Wraps existing code with zero changes to runner/, krun/, or propolis-runner:

  • PrepareRootFS(): writes .krun_config.json via existing image.KrunConfig.WriteTo()
  • Start(): calls existing runner.SpawnProcess(), wraps *runner.Process as VMHandle
  • ID(): returns PID as string

Package layout

hypervisor/
    backend.go                          # Backend, VMHandle, VMConfig, InitConfig, NetEndpoint
    default_{linux,darwin,windows}.go   # Platform default backend name
    doc.go
    libkrun/
        backend.go                      # Wraps runner.SpawnProcess + image.KrunConfig
        doc.go

What stays unchanged

  • runner/ package (spawn.go, config.go, interfaces.go, process_*.go)
  • krun/ package (context.go)
  • runner/cmd/propolis-runner/ binary
  • image/, net/, ssh/, state/, hooks/, extract/ packages
  • All existing tests

Acceptance criteria

  • hypervisor package with Backend, VMHandle, VMConfig, InitConfig, NetEndpoint types
  • hypervisor/libkrun package implementing Backend by wrapping existing runner code
  • propolis.Run() uses Backend interface instead of direct krun config / spawner calls
  • WithBackend() option added; WithSpawner/WithLibDir/WithRunnerPath deprecated
  • VM struct uses VMHandle; ID() method added, PID() deprecated
  • All existing tests pass with no behavioral changes
  • Platform default selection via build tags (default_{linux,darwin}.go)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions