Repository: lima-vm/lima

Issue:
Define the common `interface` for `qemuimgutil` and `nativeimgutil`
https://github.com/lima-vm/lima/blob/master/pkg/qemu/imgutil/imgutil.go
https://github.com/lima-vm/lima/blob/master/pkg/nativeimgutil/nativeimgutil.go

These package should implement the common interface.

- `pkg/imgutil`: define the interface`
- `pkg/imgutil/nativeimgutil`: native implementation
- `pkg/imgutil/qemuimgutil`: qemu-img implementation

Probably there should be also `pkg/imgutil/proxyimgutil` (name is tentative) that attempts qemuimgutil first and then fall back to nativeimgutil?


Hints from issue comments:
@Horiodino: /assign  @AkihiroSuda  is this issue open or anyone working on this ?

Solution patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 72a95673..49ad8901 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -91,7 +91,7 @@ jobs:
     - name: Run golangci-lint
       uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9  # v8.0.0
       with:
-        version: v2.1
+        version: v2.2
         args: --verbose
 
   security:
diff --git a/.golangci.yml b/.golangci.yml
index 3634b2b9..2c032917 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -81,6 +81,7 @@ linters:
       confidence: 0.6
       # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
       rules:
+      - name: bare-return
       - name: blank-imports
       - name: context-as-argument
       - name: context-keys-type
@@ -106,6 +107,7 @@ linters:
       - name: superfluous-else
       - name: time-naming
       - name: unexported-return
+      - name: unnecessary-format
       - name: unreachable-code
       - name: unused-parameter
       - name: use-any
diff --git a/cmd/limactl/disk.go b/cmd/limactl/disk.go
index cf95c016..7f19b069 100644
--- a/cmd/limactl/disk.go
+++ b/cmd/limactl/disk.go
@@ -18,8 +18,7 @@ import (
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 
-	"github.com/lima-vm/lima/pkg/nativeimgutil"
-	"github.com/lima-vm/lima/pkg/qemu/imgutil"
+	"github.com/lima-vm/lima/pkg/imgutil/proxyimgutil"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 )
@@ -113,11 +112,8 @@ func diskCreateAction(cmd *cobra.Command, args []string) error {
 
 	// qemu may not be available, use it only if needed.
 	dataDisk := filepath.Join(diskDir, filenames.DataDisk)
-	if format == "raw" {
-		err = nativeimgutil.CreateRawDisk(dataDisk, int(diskSize))
-	} else {
-		err = imgutil.CreateDisk(dataDisk, format, int(diskSize))
-	}
+	diskUtil := proxyimgutil.NewDiskUtil()
+	err = diskUtil.CreateDisk(dataDisk, diskSize)
 	if err != nil {
 		rerr := os.RemoveAll(diskDir)
 		if rerr != nil {
@@ -410,11 +406,8 @@ func diskResizeAction(cmd *cobra.Command, args []string) error {
 
 	// qemu may not be available, use it only if needed.
 	dataDisk := filepath.Join(disk.Dir, filenames.DataDisk)
-	if disk.Format == "raw" {
-		err = nativeimgutil.ResizeRawDisk(dataDisk, int(diskSize))
-	} else {
-		err = imgutil.ResizeDisk(dataDisk, disk.Format, int(diskSize))
-	}
+	diskUtil := proxyimgutil.NewDiskUtil()
+	err = diskUtil.ResizeDisk(dataDisk, diskSize)
 	if err != nil {
 		return fmt.Errorf("failed to resize disk %q: %w", diskName, err)
 	}
diff --git a/cmd/limactl/tunnel.go b/cmd/limactl/tunnel.go
index 45137617..80c15554 100644
--- a/cmd/limactl/tunnel.go
+++ b/cmd/limactl/tunnel.go
@@ -118,14 +118,14 @@ func tunnelAction(cmd *cobra.Command, args []string) error {
 
 	switch runtime.GOOS {
 	case "darwin":
-		fmt.Fprintf(stdout, "Open <System Settings> → <Network> → <Wi-Fi> (or whatever) → <Details> → <Proxies> → <SOCKS proxy>,\n")
-		fmt.Fprintf(stdout, "and specify the following configuration:\n")
-		fmt.Fprintf(stdout, "- Server: 127.0.0.1\n")
+		fmt.Fprint(stdout, "Open <System Settings> → <Network> → <Wi-Fi> (or whatever) → <Details> → <Proxies> → <SOCKS proxy>,\n")
+		fmt.Fprint(stdout, "and specify the following configuration:\n")
+		fmt.Fprint(stdout, "- Server: 127.0.0.1\n")
 		fmt.Fprintf(stdout, "- Port: %d\n", port)
 	case "windows":
-		fmt.Fprintf(stdout, "Open <Settings> → <Network & Internet> → <Proxy>,\n")
-		fmt.Fprintf(stdout, "and specify the following configuration:\n")
-		fmt.Fprintf(stdout, "- Address: socks=127.0.0.1\n")
+		fmt.Fprint(stdout, "Open <Settings> → <Network & Internet> → <Proxy>,\n")
+		fmt.Fprint(stdout, "and specify the following configuration:\n")
+		fmt.Fprint(stdout, "- Address: socks=127.0.0.1\n")
 		fmt.Fprintf(stdout, "- Port: %d\n", port)
 	default:
 		fmt.Fprintf(stdout, "Set `ALL_PROXY=socks5h://127.0.0.1:%d`, etc.\n", port)
diff --git a/pkg/guestagent/guestagent_linux.go b/pkg/guestagent/guestagent_linux.go
index 2013c611..40881adb 100644
--- a/pkg/guestagent/guestagent_linux.go
+++ b/pkg/guestagent/guestagent_linux.go
@@ -169,7 +169,7 @@ func comparePorts(old, neww []*api.IPPort) (added, removed []*api.IPPort) {
 			}
 		}
 	}
-	return
+	return added, removed
 }
 
 func (a *agent) collectEvent(ctx context.Context, st eventState) (*api.Event, eventState) {
diff --git a/pkg/guestagent/timesync/timesync_linux.go b/pkg/guestagent/timesync/timesync_linux.go
index af613006..0a7c7c83 100644
--- a/pkg/guestagent/timesync/timesync_linux.go
+++ b/pkg/guestagent/timesync/timesync_linux.go
@@ -18,18 +18,17 @@ func HasRTC() (bool, error) {
 	return !errors.Is(err, os.ErrNotExist), err
 }
 
-func GetRTCTime() (t time.Time, err error) {
+func GetRTCTime() (time.Time, error) {
 	f, err := os.Open(rtc)
 	if err != nil {
-		return
+		return time.Time{}, err
 	}
 	defer f.Close()
 	obj, err := unix.IoctlGetRTCTime(int(f.Fd()))
 	if err != nil {
-		return
+		return time.Time{}, err
 	}
-	t = time.Date(int(obj.Year+1900), time.Month(obj.Mon+1), int(obj.Mday), int(obj.Hour), int(obj.Min), int(obj.Sec), 0, time.UTC)
-	return t, nil
+	return time.Date(int(obj.Year+1900), time.Month(obj.Mon+1), int(obj.Mday), int(obj.Hour), int(obj.Min), int(obj.Sec), 0, time.UTC), nil
 }
 
 func SetSystemTime(t time.Time) error {
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index a6e293e1..88491d9b 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -222,14 +222,11 @@ func writeSSHConfigFile(sshPath, instName, instDir, instSSHAddress string, sshLo
 	if instDir == "" {
 		return fmt.Errorf("directory is unknown for the instance %q", instName)
 	}
-	var b bytes.Buffer
-	if _, err := fmt.Fprintf(&b, `# This SSH config file can be passed to 'ssh -F'.
+	b := bytes.NewBufferString(`# This SSH config file can be passed to 'ssh -F'.
 # This file is created by Lima, but not used by Lima itself currently.
 # Modifications to this file will be lost on restarting the Lima instance.
-`); err != nil {
-		return err
-	}
-	if err := sshutil.Format(&b, sshPath, instName, sshutil.FormatConfig,
+`)
+	if err := sshutil.Format(b, sshPath, instName, sshutil.FormatConfig,
 		append(sshOpts,
 			fmt.Sprintf("Hostname=%s", instSSHAddress),
 			fmt.Sprintf("Port=%d", sshLocalPort),
diff --git a/pkg/imgutil/manager.go b/pkg/imgutil/manager.go
new file mode 100644
index 00000000..a7f16ad7
--- /dev/null
+++ b/pkg/imgutil/manager.go
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package imgutil
+
+import (
+	"os"
+)
+
+// ImageDiskManager defines the common operations for disk image utilities.
+type ImageDiskManager interface {
+	// CreateDisk creates a new disk image with the specified size.
+	CreateDisk(disk string, size int64) error
+
+	// ResizeDisk resizes an existing disk image to the specified size.
+	ResizeDisk(disk string, size int64) error
+
+	// ConvertToRaw converts a disk image to raw format.
+	ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error
+
+	// MakeSparse makes a file sparse, starting from the specified offset.
+	MakeSparse(f *os.File, offset int64) error
+}
diff --git a/pkg/nativeimgutil/nativeimgutil.go b/pkg/imgutil/nativeimgutil/nativeimgutil.go
similarity index 72%
rename from pkg/nativeimgutil/nativeimgutil.go
rename to pkg/imgutil/nativeimgutil/nativeimgutil.go
index b4ccb0ea..910512d9 100644
--- a/pkg/nativeimgutil/nativeimgutil.go
+++ b/pkg/imgutil/nativeimgutil/nativeimgutil.go
@@ -28,36 +28,19 @@ import (
 // aligned to 512 bytes.
 const sectorSize = 512
 
-// RoundUp rounds size up to sectorSize.
-func RoundUp(size int) int {
+// NativeImageUtil is the native implementation of the imgutil.ImageDiskManager.
+type NativeImageUtil struct{}
+
+// roundUp rounds size up to sectorSize.
+func roundUp(size int64) int64 {
 	sectors := (size + sectorSize - 1) / sectorSize
 	return sectors * sectorSize
 }
 
-// CreateRawDisk creates an empty raw data disk.
-func CreateRawDisk(disk string, size int) error {
-	if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
-		return err
-	}
-	f, err := os.Create(disk)
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-	roundedSize := RoundUp(size)
-	return f.Truncate(int64(roundedSize))
-}
-
-// ResizeRawDisk resizes a raw data disk.
-func ResizeRawDisk(disk string, size int) error {
-	roundedSize := RoundUp(size)
-	return os.Truncate(disk, int64(roundedSize))
-}
-
-// ConvertToRaw converts a source disk into a raw disk.
+// convertToRaw converts a source disk into a raw disk.
 // source and dest may be same.
-// ConvertToRaw is a NOP if source == dest, and no resizing is needed.
-func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
+// convertToRaw is a NOP if source == dest, and no resizing is needed.
+func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
 	srcF, err := os.Open(source)
 	if err != nil {
 		return err
@@ -106,7 +89,7 @@ func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
 	// Truncating before copy eliminates the seeks during copy and provide a
 	// hint to the file system that may minimize allocations and fragmentation
 	// of the file.
-	if err := MakeSparse(destTmpF, srcImg.Size()); err != nil {
+	if err := makeSparse(destTmpF, srcImg.Size()); err != nil {
 		return err
 	}
 
@@ -125,7 +108,7 @@ func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
 	// Resize
 	if size != nil {
 		logrus.Infof("Expanding to %s", units.BytesSize(float64(*size)))
-		if err = MakeSparse(destTmpF, *size); err != nil {
+		if err = makeSparse(destTmpF, *size); err != nil {
 			return err
 		}
 	}
@@ -153,7 +136,7 @@ func convertRawToRaw(source, dest string, size *int64) error {
 		if err != nil {
 			return err
 		}
-		if err = MakeSparse(destF, *size); err != nil {
+		if err = makeSparse(destF, *size); err != nil {
 			_ = destF.Close()
 			return err
 		}
@@ -162,9 +145,39 @@ func convertRawToRaw(source, dest string, size *int64) error {
 	return nil
 }
 
-func MakeSparse(f *os.File, n int64) error {
-	if _, err := f.Seek(n, io.SeekStart); err != nil {
+func makeSparse(f *os.File, offset int64) error {
+	if _, err := f.Seek(offset, io.SeekStart); err != nil {
 		return err
 	}
-	return f.Truncate(n)
+	return f.Truncate(offset)
+}
+
+// CreateDisk creates a new disk image with the specified size.
+func (n *NativeImageUtil) CreateDisk(disk string, size int64) error {
+	if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
+		return err
+	}
+	f, err := os.Create(disk)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	roundedSize := roundUp(size)
+	return f.Truncate(int64(roundedSize))
+}
+
+// ConvertToRaw converts a disk image to raw format.
+func (n *NativeImageUtil) ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
+	return convertToRaw(source, dest, size, allowSourceWithBackingFile)
+}
+
+// ResizeDisk resizes an existing disk image to the specified size.
+func (n *NativeImageUtil) ResizeDisk(disk string, size int64) error {
+	roundedSize := roundUp(size)
+	return os.Truncate(disk, roundedSize)
+}
+
+// MakeSparse makes a file sparse, starting from the specified offset.
+func (n *NativeImageUtil) MakeSparse(f *os.File, offset int64) error {
+	return makeSparse(f, offset)
 }
diff --git a/pkg/imgutil/proxyimgutil/proxyimgutil.go b/pkg/imgutil/proxyimgutil/proxyimgutil.go
new file mode 100644
index 00000000..7ff88abc
--- /dev/null
+++ b/pkg/imgutil/proxyimgutil/proxyimgutil.go
@@ -0,0 +1,75 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package proxyimgutil
+
+import (
+	"errors"
+	"os"
+	"os/exec"
+
+	"github.com/lima-vm/lima/pkg/imgutil"
+	"github.com/lima-vm/lima/pkg/imgutil/nativeimgutil"
+	"github.com/lima-vm/lima/pkg/imgutil/qemuimgutil"
+)
+
+// ImageDiskManager is a proxy implementation of imgutil.ImageDiskManager that uses both QEMU and native image utilities.
+type ImageDiskManager struct {
+	qemu   imgutil.ImageDiskManager
+	native imgutil.ImageDiskManager
+}
+
+// NewDiskUtil returns a new instance of ImageDiskManager that uses both QEMU and native image utilities.
+func NewDiskUtil() imgutil.ImageDiskManager {
+	return &ImageDiskManager{
+		qemu:   &qemuimgutil.QemuImageUtil{DefaultFormat: qemuimgutil.QemuImgFormat},
+		native: &nativeimgutil.NativeImageUtil{},
+	}
+}
+
+// CreateDisk creates a new disk image with the specified size.
+func (p *ImageDiskManager) CreateDisk(disk string, size int64) error {
+	err := p.qemu.CreateDisk(disk, size)
+	if err == nil {
+		return nil
+	}
+	if errors.Is(err, exec.ErrNotFound) {
+		return p.native.CreateDisk(disk, size)
+	}
+	return err
+}
+
+// ResizeDisk resizes an existing disk image to the specified size.
+func (p *ImageDiskManager) ResizeDisk(disk string, size int64) error {
+	err := p.qemu.ResizeDisk(disk, size)
+	if err == nil {
+		return nil
+	}
+	if errors.Is(err, exec.ErrNotFound) {
+		return p.native.ResizeDisk(disk, size)
+	}
+	return err
+}
+
+// ConvertToRaw converts a disk image to raw format.
+func (p *ImageDiskManager) ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
+	err := p.qemu.ConvertToRaw(source, dest, size, allowSourceWithBackingFile)
+	if err == nil {
+		return nil
+	}
+	if errors.Is(err, exec.ErrNotFound) {
+		return p.native.ConvertToRaw(source, dest, size, allowSourceWithBackingFile)
+	}
+	return err
+}
+
+func (p *ImageDiskManager) MakeSparse(f *os.File, offset int64) error {
+	err := p.qemu.MakeSparse(f, offset)
+	if err == nil {
+		return nil
+	}
+	if errors.Is(err, exec.ErrNotFound) {
+		return p.native.MakeSparse(f, offset)
+	}
+	return err
+}
diff --git a/pkg/qemu/imgutil/imgutil.go b/pkg/imgutil/qemuimgutil/qemuimgutil.go
similarity index 72%
rename from pkg/qemu/imgutil/imgutil.go
rename to pkg/imgutil/qemuimgutil/qemuimgutil.go
index 18512bf0..fc065450 100644
--- a/pkg/qemu/imgutil/imgutil.go
+++ b/pkg/imgutil/qemuimgutil/qemuimgutil.go
@@ -1,7 +1,7 @@
 // SPDX-FileCopyrightText: Copyright The Lima Authors
 // SPDX-License-Identifier: Apache-2.0
 
-package imgutil
+package qemuimgutil
 
 import (
 	"bytes"
@@ -16,6 +16,28 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
+const QemuImgFormat = "qcow2"
+
+// QemuImageUtil is the QEMU implementation of the imgutil Interface.
+type QemuImageUtil struct {
+	DefaultFormat string // Default disk format, e.g., "qcow2"
+}
+
+// Info corresponds to the output of `qemu-img info --output=json FILE`.
+type Info struct {
+	Filename              string              `json:"filename,omitempty"`                // since QEMU 1.3
+	Format                string              `json:"format,omitempty"`                  // since QEMU 1.3
+	VSize                 int64               `json:"virtual-size,omitempty"`            // since QEMU 1.3
+	ActualSize            int64               `json:"actual-size,omitempty"`             // since QEMU 1.3
+	DirtyFlag             bool                `json:"dirty-flag,omitempty"`              // since QEMU 5.2
+	ClusterSize           int                 `json:"cluster-size,omitempty"`            // since QEMU 1.3
+	BackingFilename       string              `json:"backing-filename,omitempty"`        // since QEMU 1.3
+	FullBackingFilename   string              `json:"full-backing-filename,omitempty"`   // since QEMU 1.3
+	BackingFilenameFormat string              `json:"backing-filename-format,omitempty"` // since QEMU 1.3
+	FormatSpecific        *InfoFormatSpecific `json:"format-specific,omitempty"`         // since QEMU 1.7
+	Children              []InfoChild         `json:"children,omitempty"`                // since QEMU 8.0
+}
+
 type InfoChild struct {
 	Name string `json:"name,omitempty"` // since QEMU 8.0
 	Info Info   `json:"info,omitempty"` // since QEMU 8.0
@@ -26,22 +48,8 @@ type InfoFormatSpecific struct {
 	Data json.RawMessage `json:"data,omitempty"` // since QEMU 1.7
 }
 
-func CreateDisk(disk, format string, size int) error {
-	if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
-		// disk already exists
-		return err
-	}
-
-	args := []string{"create", "-f", format, disk, strconv.Itoa(size)}
-	cmd := exec.Command("qemu-img", args...)
-	if out, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
-	}
-	return nil
-}
-
-func ResizeDisk(disk, format string, size int) error {
-	args := []string{"resize", "-f", format, disk, strconv.Itoa(size)}
+func resizeDisk(disk, format string, size int64) error {
+	args := []string{"resize", "-f", format, disk, strconv.FormatInt(size, 10)}
 	cmd := exec.Command("qemu-img", args...)
 	if out, err := cmd.CombinedOutput(); err != nil {
 		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
@@ -94,22 +102,7 @@ type InfoFormatSpecificDataVmdkExtent struct {
 	ClusterSize int    `json:"cluster-size,omitempty"` // since QEMU 1.7
 }
 
-// Info corresponds to the output of `qemu-img info --output=json FILE`.
-type Info struct {
-	Filename              string              `json:"filename,omitempty"`                // since QEMU 1.3
-	Format                string              `json:"format,omitempty"`                  // since QEMU 1.3
-	VSize                 int64               `json:"virtual-size,omitempty"`            // since QEMU 1.3
-	ActualSize            int64               `json:"actual-size,omitempty"`             // since QEMU 1.3
-	DirtyFlag             bool                `json:"dirty-flag,omitempty"`              // since QEMU 5.2
-	ClusterSize           int                 `json:"cluster-size,omitempty"`            // since QEMU 1.3
-	BackingFilename       string              `json:"backing-filename,omitempty"`        // since QEMU 1.3
-	FullBackingFilename   string              `json:"full-backing-filename,omitempty"`   // since QEMU 1.3
-	BackingFilenameFormat string              `json:"backing-filename-format,omitempty"` // since QEMU 1.3
-	FormatSpecific        *InfoFormatSpecific `json:"format-specific,omitempty"`         // since QEMU 1.7
-	Children              []InfoChild         `json:"children,omitempty"`                // since QEMU 8.0
-}
-
-func ConvertToRaw(source, dest string) error {
+func convertToRaw(source, dest string) error {
 	var stdout, stderr bytes.Buffer
 	cmd := exec.Command("qemu-img", "convert", "-O", "raw", source, dest)
 	cmd.Stdout = &stdout
@@ -121,7 +114,7 @@ func ConvertToRaw(source, dest string) error {
 	return nil
 }
 
-func ParseInfo(b []byte) (*Info, error) {
+func parseInfo(b []byte) (*Info, error) {
 	var imgInfo Info
 	if err := json.Unmarshal(b, &imgInfo); err != nil {
 		return nil, err
@@ -129,7 +122,7 @@ func ParseInfo(b []byte) (*Info, error) {
 	return &imgInfo, nil
 }
 
-func GetInfo(f string) (*Info, error) {
+func getInfo(f string) (*Info, error) {
 	var stdout, stderr bytes.Buffer
 	cmd := exec.Command("qemu-img", "info", "--output=json", "--force-share", f)
 	cmd.Stdout = &stdout
@@ -138,10 +131,80 @@ func GetInfo(f string) (*Info, error) {
 		return nil, fmt.Errorf("failed to run %v: stdout=%q, stderr=%q: %w",
 			cmd.Args, stdout.String(), stderr.String(), err)
 	}
-	return ParseInfo(stdout.Bytes())
+	return parseInfo(stdout.Bytes())
+}
+
+// CreateDisk creates a new disk image with the specified size.
+func (q *QemuImageUtil) CreateDisk(disk string, size int64) error {
+	if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
+		// disk already exists
+		return err
+	}
+
+	args := []string{"create", "-f", q.DefaultFormat, disk, strconv.FormatInt(size, 10)}
+	cmd := exec.Command("qemu-img", args...)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
+	}
+	return nil
+}
+
+// ResizeDisk resizes an existing disk image to the specified size.
+func (q *QemuImageUtil) ResizeDisk(disk string, size int64) error {
+	info, err := getInfo(disk)
+	if err != nil {
+		return fmt.Errorf("failed to get info for disk %q: %w", disk, err)
+	}
+	return resizeDisk(disk, info.Format, size)
+}
+
+// MakeSparse is a stub implementation as the qemu package doesn't provide this functionality.
+func (q *QemuImageUtil) MakeSparse(_ *os.File, _ int64) error {
+	return nil
+}
+
+// GetInfo retrieves the information of a disk image.
+func GetInfo(path string) (*Info, error) {
+	qemuInfo, err := getInfo(path)
+	if err != nil {
+		return nil, err
+	}
+
+	return qemuInfo, nil
+}
+
+// ConvertToRaw converts a disk image to raw format.
+func (q *QemuImageUtil) ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
+	if !allowSourceWithBackingFile {
+		info, err := getInfo(source)
+		if err != nil {
+			return fmt.Errorf("failed to get info for source disk %q: %w", source, err)
+		}
+		if info.BackingFilename != "" || info.FullBackingFilename != "" {
+			return fmt.Errorf("qcow2 image %q has an unexpected backing file: %q", source, info.BackingFilename)
+		}
+	}
+
+	if err := convertToRaw(source, dest); err != nil {
+		return err
+	}
+
+	if size != nil {
+		destInfo, err := getInfo(dest)
+		if err != nil {
+			return fmt.Errorf("failed to get info for converted disk %q: %w", dest, err)
+		}
+
+		if *size > destInfo.VSize {
+			return resizeDisk(dest, "raw", *size)
+		}
+	}
+
+	return nil
 }
 
-func AcceptableAsBasedisk(info *Info) error {
+// AcceptableAsBaseDisk checks if a disk image is acceptable as a base disk.
+func AcceptableAsBaseDisk(info *Info) error {
 	switch info.Format {
 	case "qcow2", "raw":
 		// NOP
diff --git a/pkg/instance/start.go b/pkg/instance/start.go
index 1df00c78..acd3741e 100644
--- a/pkg/instance/start.go
+++ b/pkg/instance/start.go
@@ -27,10 +27,9 @@ import (
 	"github.com/lima-vm/lima/pkg/executil"
 	"github.com/lima-vm/lima/pkg/fileutils"
 	hostagentevents "github.com/lima-vm/lima/pkg/hostagent/events"
+	"github.com/lima-vm/lima/pkg/imgutil/proxyimgutil"
 	"github.com/lima-vm/lima/pkg/limayaml"
-	"github.com/lima-vm/lima/pkg/nativeimgutil"
 	"github.com/lima-vm/lima/pkg/osutil"
-	"github.com/lima-vm/lima/pkg/qemu/imgutil"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 	"github.com/lima-vm/lima/pkg/usrlocalsharelima"
@@ -414,7 +413,6 @@ func prepareDiffDisk(inst *store.Instance) error {
 	}
 
 	diskSize := img.Size()
-	format := string(img.Type())
 
 	if inst.Disk == diskSize {
 		return nil
@@ -427,12 +425,9 @@ func prepareDiffDisk(inst *store.Instance) error {
 		return errors.New("diffDisk: Shrinking is currently unavailable")
 	}
 
-	if format == "raw" {
-		err = nativeimgutil.ResizeRawDisk(diffDisk, int(inst.Disk))
-	} else {
-		err = imgutil.ResizeDisk(diffDisk, format, int(inst.Disk))
-	}
+	diskUtil := proxyimgutil.NewDiskUtil()
 
+	err = diskUtil.ResizeDisk(diffDisk, inst.Disk)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/portfwd/control_others.go b/pkg/portfwd/control_others.go
index d6e666ac..529b0750 100644
--- a/pkg/portfwd/control_others.go
+++ b/pkg/portfwd/control_others.go
@@ -26,5 +26,5 @@ func Control(_, _ string, c syscall.RawConn) (err error) {
 	if controlErr != nil {
 		err = controlErr
 	}
-	return
+	return err
 }
diff --git a/pkg/portfwd/control_windows.go b/pkg/portfwd/control_windows.go
index db41ab11..57b09cd9 100644
--- a/pkg/portfwd/control_windows.go
+++ b/pkg/portfwd/control_windows.go
@@ -19,5 +19,5 @@ func Control(_, _ string, c syscall.RawConn) (err error) {
 	if controlErr != nil {
 		err = controlErr
 	}
-	return
+	return err
 }
diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go
index 0ba01b61..8a54ba8a 100644
--- a/pkg/qemu/qemu.go
+++ b/pkg/qemu/qemu.go
@@ -28,12 +28,12 @@ import (
 	"github.com/sirupsen/logrus"
 
 	"github.com/lima-vm/lima/pkg/fileutils"
+	"github.com/lima-vm/lima/pkg/imgutil/qemuimgutil"
 	"github.com/lima-vm/lima/pkg/iso9660util"
 	"github.com/lima-vm/lima/pkg/limayaml"
 	"github.com/lima-vm/lima/pkg/networks"
 	"github.com/lima-vm/lima/pkg/networks/usernet"
 	"github.com/lima-vm/lima/pkg/osutil"
-	"github.com/lima-vm/lima/pkg/qemu/imgutil"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 )
@@ -137,11 +137,11 @@ func EnsureDisk(ctx context.Context, cfg Config) error {
 	if err != nil {
 		return err
 	}
-	baseDiskInfo, err := imgutil.GetInfo(baseDisk)
+	baseDiskInfo, err := qemuimgutil.GetInfo(baseDisk)
 	if err != nil {
 		return fmt.Errorf("failed to get the information of base disk %q: %w", baseDisk, err)
 	}
-	if err = imgutil.AcceptableAsBasedisk(baseDiskInfo); err != nil {
+	if err = qemuimgutil.AcceptableAsBaseDisk(baseDiskInfo); err != nil {
 		return fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err)
 	}
 	if baseDiskInfo.Format == "" {
@@ -691,11 +691,11 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 	if diskSize, _ := units.RAMInBytes(*cfg.LimaYAML.Disk); diskSize > 0 {
 		args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio,discard=on", diffDisk))
 	} else if !isBaseDiskCDROM {
-		baseDiskInfo, err := imgutil.GetInfo(baseDisk)
+		baseDiskInfo, err := qemuimgutil.GetInfo(baseDisk)
 		if err != nil {
 			return "", nil, fmt.Errorf("failed to get the information of %q: %w", baseDisk, err)
 		}
-		if err = imgutil.AcceptableAsBasedisk(baseDiskInfo); err != nil {
+		if err = qemuimgutil.AcceptableAsBaseDisk(baseDiskInfo); err != nil {
 			return "", nil, fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err)
 		}
 		if baseDiskInfo.Format == "" {
diff --git a/pkg/vz/disk.go b/pkg/vz/disk.go
index 3fff17c4..06194faf 100644
--- a/pkg/vz/disk.go
+++ b/pkg/vz/disk.go
@@ -14,8 +14,8 @@ import (
 
 	"github.com/lima-vm/lima/pkg/driver"
 	"github.com/lima-vm/lima/pkg/fileutils"
+	"github.com/lima-vm/lima/pkg/imgutil/proxyimgutil"
 	"github.com/lima-vm/lima/pkg/iso9660util"
-	"github.com/lima-vm/lima/pkg/nativeimgutil"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 )
 
@@ -26,6 +26,8 @@ func EnsureDisk(ctx context.Context, driver *driver.BaseDriver) error {
 		return err
 	}
 
+	diskUtil := proxyimgutil.NewDiskUtil()
+
 	baseDisk := filepath.Join(driver.Instance.Dir, filenames.BaseDisk)
 	kernel := filepath.Join(driver.Instance.Dir, filenames.Kernel)
 	kernelCmdline := filepath.Join(driver.Instance.Dir, filenames.KernelCmdline)
@@ -78,13 +80,15 @@ func EnsureDisk(ctx context.Context, driver *driver.BaseDriver) error {
 		if err != nil {
 			return err
 		}
-		if err = nativeimgutil.MakeSparse(diffDiskF, diskSize); err != nil {
+
+		err = diskUtil.MakeSparse(diffDiskF, 0)
+		if err != nil {
 			diffDiskF.Close()
-			return err
+			return fmt.Errorf("failed to create sparse diff disk %q: %w", diffDisk, err)
 		}
 		return diffDiskF.Close()
 	}
-	if err = nativeimgutil.ConvertToRaw(baseDisk, diffDisk, &diskSize, false); err != nil {
+	if err = diskUtil.ConvertToRaw(baseDisk, diffDisk, &diskSize, false); err != nil {
 		return fmt.Errorf("failed to convert %q to a raw disk %q: %w", baseDisk, diffDisk, err)
 	}
 	return err
diff --git a/pkg/vz/vm_darwin.go b/pkg/vz/vm_darwin.go
index 1f21cf44..9ea33a86 100644
--- a/pkg/vz/vm_darwin.go
+++ b/pkg/vz/vm_darwin.go
@@ -25,9 +25,9 @@ import (
 	"github.com/sirupsen/logrus"
 
 	"github.com/lima-vm/lima/pkg/driver"
+	"github.com/lima-vm/lima/pkg/imgutil/proxyimgutil"
 	"github.com/lima-vm/lima/pkg/iso9660util"
 	"github.com/lima-vm/lima/pkg/limayaml"
-	"github.com/lima-vm/lima/pkg/nativeimgutil"
 	"github.com/lima-vm/lima/pkg/networks"
 	"github.com/lima-vm/lima/pkg/networks/usernet"
 	"github.com/lima-vm/lima/pkg/osutil"
@@ -471,6 +471,8 @@ func attachDisks(driver *driver.BaseDriver, vmConfig *vz.VirtualMachineConfigura
 	}
 	configurations = append(configurations, diffDisk)
 
+	diskUtil := proxyimgutil.NewDiskUtil()
+
 	for _, d := range driver.Instance.Config.AdditionalDisks {
 		diskName := d.Name
 		disk, err := store.InspectDisk(diskName)
@@ -489,7 +491,8 @@ func attachDisks(driver *driver.BaseDriver, vmConfig *vz.VirtualMachineConfigura
 		extraDiskPath := filepath.Join(disk.Dir, filenames.DataDisk)
 		// ConvertToRaw is a NOP if no conversion is needed
 		logrus.Debugf("Converting extra disk %q to a raw disk (if it is not a raw)", extraDiskPath)
-		if err = nativeimgutil.ConvertToRaw(extraDiskPath, extraDiskPath, nil, true); err != nil {
+
+		if err = diskUtil.ConvertToRaw(extraDiskPath, extraDiskPath, nil, true); err != nil {
 			return fmt.Errorf("failed to convert extra disk %q to a raw disk: %w", extraDiskPath, err)
 		}
 		extraDiskPathAttachment, err := vz.NewDiskImageStorageDeviceAttachmentWithCacheAndSync(extraDiskPath, false, diskImageCachingMode, vz.DiskImageSynchronizationModeFsync)
diff --git a/templates/README.md b/templates/README.md
index f6fe4c65..04fed74e 100644
--- a/templates/README.md
+++ b/templates/README.md
@@ -54,7 +54,7 @@ Container image builders:
 - [`buildkit`](./buildkit.yaml): BuildKit
 
 Container orchestration:
-- [`faasd`](./faasd.yaml): [Faasd](https://docs.openfaas.com/deployment/faasd/)
+- [`faasd`](./faasd.yaml): [Faasd](https://docs.openfaas.com/deployment/edge/)
 - [`k3s`](./k3s.yaml): Kubernetes via k3s
 - [`k8s`](./k8s.yaml): Kubernetes via kubeadm
 - [`experimental/u7s`](./experimental/u7s.yaml): [Usernetes](https://github.com/rootless-containers/usernetes): Rootless Kubernetes



Test patch:
diff --git a/pkg/nativeimgutil/fuzz_test.go b/pkg/imgutil/nativeimgutil/fuzz_test.go
similarity index 89%
rename from pkg/nativeimgutil/fuzz_test.go
rename to pkg/imgutil/nativeimgutil/fuzz_test.go
index 7a3face5..204d5583 100644
--- a/pkg/nativeimgutil/fuzz_test.go
+++ b/pkg/imgutil/nativeimgutil/fuzz_test.go
@@ -17,6 +17,6 @@ func FuzzConvertToRaw(f *testing.F) {
 		destPath := filepath.Join(t.TempDir(), "dest.img")
 		err := os.WriteFile(srcPath, imgData, 0o600)
 		assert.NilError(t, err)
-		_ = ConvertToRaw(srcPath, destPath, &size, withBacking)
+		_ = convertToRaw(srcPath, destPath, &size, withBacking)
 	})
 }
diff --git a/pkg/nativeimgutil/nativeimgutil_test.go b/pkg/imgutil/nativeimgutil/nativeimgutil_test.go
similarity index 89%
rename from pkg/nativeimgutil/nativeimgutil_test.go
rename to pkg/imgutil/nativeimgutil/nativeimgutil_test.go
index 943a8a8c..466edc62 100644
--- a/pkg/nativeimgutil/nativeimgutil_test.go
+++ b/pkg/imgutil/nativeimgutil/nativeimgutil_test.go
@@ -15,8 +15,8 @@ import (
 
 func TestRoundUp(t *testing.T) {
 	tests := []struct {
-		Size    int
-		Rounded int
+		Size    int64
+		Rounded int64
 	}{
 		{0, 0},
 		{1, 512},
@@ -25,7 +25,7 @@ func TestRoundUp(t *testing.T) {
 		{123456789, 123457024},
 	}
 	for _, tc := range tests {
-		if RoundUp(tc.Size) != tc.Rounded {
+		if roundUp(tc.Size) != tc.Rounded {
 			t.Errorf("expected %d, got %d", tc.Rounded, tc.Size)
 		}
 	}
@@ -63,7 +63,7 @@ func TestConvertToRaw(t *testing.T) {
 	t.Run("qcow without backing file", func(t *testing.T) {
 		resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))
 
-		err = ConvertToRaw(qcowImage.Name(), resultImage, nil, false)
+		err = convertToRaw(qcowImage.Name(), resultImage, nil, false)
 		assert.NilError(t, err)
 		assertFileEquals(t, rawImage.Name(), resultImage)
 	})
@@ -71,7 +71,7 @@ func TestConvertToRaw(t *testing.T) {
 	t.Run("qcow with backing file", func(t *testing.T) {
 		resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))
 
-		err = ConvertToRaw(qcowImage.Name(), resultImage, nil, true)
+		err = convertToRaw(qcowImage.Name(), resultImage, nil, true)
 		assert.NilError(t, err)
 		assertFileEquals(t, rawImage.Name(), resultImage)
 	})
@@ -80,7 +80,7 @@ func TestConvertToRaw(t *testing.T) {
 		resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))
 
 		size := int64(2_097_152) // 2mb
-		err = ConvertToRaw(qcowImage.Name(), resultImage, &size, false)
+		err = convertToRaw(qcowImage.Name(), resultImage, &size, false)
 		assert.NilError(t, err)
 		assertFileEquals(t, rawImageExtended.Name(), resultImage)
 	})
@@ -88,7 +88,7 @@ func TestConvertToRaw(t *testing.T) {
 	t.Run("raw", func(t *testing.T) {
 		resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))
 
-		err = ConvertToRaw(rawImage.Name(), resultImage, nil, false)
+		err = convertToRaw(rawImage.Name(), resultImage, nil, false)
 		assert.NilError(t, err)
 		assertFileEquals(t, rawImage.Name(), resultImage)
 	})
diff --git a/pkg/qemu/imgutil/imgutil_test.go b/pkg/imgutil/qemuimgutil/qemuimgutil_test.go
similarity index 97%
rename from pkg/qemu/imgutil/imgutil_test.go
rename to pkg/imgutil/qemuimgutil/qemuimgutil_test.go
index bb630f48..b0f39d43 100644
--- a/pkg/qemu/imgutil/imgutil_test.go
+++ b/pkg/imgutil/qemuimgutil/qemuimgutil_test.go
@@ -1,7 +1,7 @@
 // SPDX-FileCopyrightText: Copyright The Lima Authors
 // SPDX-License-Identifier: Apache-2.0
 
-package imgutil
+package qemuimgutil
 
 import (
 	"testing"
@@ -52,7 +52,7 @@ func TestParseInfo(t *testing.T) {
     "dirty-flag": false
 }`
 
-		info, err := ParseInfo([]byte(s))
+		info, err := parseInfo([]byte(s))
 		assert.NilError(t, err)
 		assert.Equal(t, 1, len(info.Children))
 		assert.Check(t, info.FormatSpecific != nil)
@@ -104,7 +104,7 @@ func TestParseInfo(t *testing.T) {
     "backing-filename": "foo.qcow2",
     "dirty-flag": false
 }`
-			info, err := ParseInfo([]byte(s))
+			info, err := parseInfo([]byte(s))
 			assert.NilError(t, err)
 			assert.Equal(t, 1, len(info.Children))
 			assert.Equal(t, "foo.qcow2", info.BackingFilename)
@@ -202,7 +202,7 @@ func TestParseInfo(t *testing.T) {
     },
     "dirty-flag": false
 }`
-			info, err := ParseInfo([]byte(s))
+			info, err := parseInfo([]byte(s))
 			assert.NilError(t, err)
 			assert.Equal(t, 3, len(info.Children))
 			assert.Equal(t, "foo.vmdk", info.Filename)
diff --git a/pkg/networks/commands_test.go b/pkg/networks/commands_test.go
index 7da2bae6..45bf0094 100644
--- a/pkg/networks/commands_test.go
+++ b/pkg/networks/commands_test.go
@@ -49,7 +49,7 @@ func TestUser(t *testing.T) {
 
 	t.Run("socket_vmnet", func(t *testing.T) {
 		if ok, _ := config.IsDaemonInstalled(SocketVMNet); !ok {
-			t.Skipf("socket_vmnet is not installed")
+			t.Skip("socket_vmnet is not installed")
 		}
 		user, err := config.User(SocketVMNet)
 		assert.NilError(t, err)
@@ -80,7 +80,7 @@ func TestStartCmd(t *testing.T) {
 
 	t.Run("socket_vmnet", func(t *testing.T) {
 		if ok, _ := config.IsDaemonInstalled(SocketVMNet); !ok {
-			t.Skipf("socket_vmnet is not installed")
+			t.Skip("socket_vmnet is not installed")
 		}
 
 		cmd := config.StartCmd("shared", SocketVMNet)
diff --git a/pkg/vz/network_darwin_test.go b/pkg/vz/network_darwin_test.go
index f29cf850..8a6659f5 100644
--- a/pkg/vz/network_darwin_test.go
+++ b/pkg/vz/network_darwin_test.go
@@ -81,7 +81,7 @@ func TestDialQemu(t *testing.T) {
 
 	buf := make([]byte, vmnetMaxPacketSize)
 
-	t.Logf("Receiving and verifying data packets...")
+	t.Log("Receiving and verifying data packets...")
 	for i := range packetsCount {
 		n, err := vzConn.Read(buf)
 		assert.NilError(t, err)