Migration Guide: hypervisor.Backend Abstraction

March 16, 2026 ยท View on GitHub

This document covers the breaking changes introduced by the hypervisor.Backend abstraction layer and how to migrate downstream consumers.

Breaking Changes

Removed APIReplacementAffected projects
microvm.WithRunnerPath(p)libkrun.WithRunnerPath(p) passed to libkrun.NewBackend()waggle, apiary, toolhive-appliance
microvm.WithLibDir(d)libkrun.WithLibDir(d) passed to libkrun.NewBackend()waggle, toolhive-appliance
microvm.WithSpawner(s)libkrun.WithSpawner(s) passed to libkrun.NewBackend()tests only
microvm.VM.PID() intmicrovm.VM.ID() stringwaggle, toolhive-appliance

New Imports

import "github.com/stacklok/go-microvm/hypervisor/libkrun"

Migration Examples

waggle (pkg/infra/vm/microvm.go)

Before:

if opts.RunnerPath != "" {
    microvmOpts = append(microvmOpts, microvm.WithRunnerPath(opts.RunnerPath))
}
if opts.LibDir != "" {
    microvmOpts = append(microvmOpts, microvm.WithLibDir(opts.LibDir))
}
slog.Info("microVM created", "pid", vm.PID())

After:

import "github.com/stacklok/go-microvm/hypervisor/libkrun"

var backendOpts []libkrun.Option
if opts.RunnerPath != "" {
    backendOpts = append(backendOpts, libkrun.WithRunnerPath(opts.RunnerPath))
}
if opts.LibDir != "" {
    backendOpts = append(backendOpts, libkrun.WithLibDir(opts.LibDir))
}
microvmOpts = append(microvmOpts, microvm.WithBackend(libkrun.NewBackend(backendOpts...)))
slog.Info("microVM created", "id", vm.ID())

apiary (internal/infra/vm/runner.go)

Before:

if r.runnerPath != "" {
    opts = append(opts, microvm.WithRunnerPath(r.runnerPath))
}

After:

import "github.com/stacklok/go-microvm/hypervisor/libkrun"

if r.runnerPath != "" {
    opts = append(opts, microvm.WithBackend(libkrun.NewBackend(
        libkrun.WithRunnerPath(r.runnerPath),
    )))
}

toolhive-appliance (internal/vm/libkrun/manager_cgo.go)

Before:

if runnerPath != "" {
    microvmOpts = append(microvmOpts, microvm.WithRunnerPath(runnerPath))
}
if libDir != "" {
    microvmOpts = append(microvmOpts, microvm.WithLibDir(libDir))
}
PID: vmInstance.PID(),
go m.reaperLoop(vmInstance.PID())

After:

import (
    "strconv"
    "github.com/stacklok/go-microvm/hypervisor/libkrun"
)

var backendOpts []libkrun.Option
if runnerPath != "" {
    backendOpts = append(backendOpts, libkrun.WithRunnerPath(runnerPath))
}
if libDir != "" {
    backendOpts = append(backendOpts, libkrun.WithLibDir(libDir))
}
microvmOpts = append(microvmOpts, microvm.WithBackend(libkrun.NewBackend(backendOpts...)))

// For PID โ€” parse ID string:
id := vmInstance.ID()
pid, _ := strconv.Atoi(id)
// Use pid for state and reaper loop

Test code using WithSpawner

Before:

spawner := &mockSpawner{proc: mockProc, err: nil}
opts := []microvm.Option{
    microvm.WithSpawner(spawner),
}

After:

// Implement hypervisor.Backend directly for test mocks:
type mockBackend struct {
    handle hypervisor.VMHandle
    err    error
}

func (m *mockBackend) Name() string { return "mock" }
func (m *mockBackend) PrepareRootFS(_ context.Context, p string, _ hypervisor.InitConfig) (string, error) {
    return p, nil
}
func (m *mockBackend) Start(_ context.Context, _ hypervisor.VMConfig) (hypervisor.VMHandle, error) {
    return m.handle, m.err
}

opts := []microvm.Option{
    microvm.WithBackend(&mockBackend{handle: mockHandle}),
}