mirror of
https://github.com/lima-vm/lima.git
synced 2026-10-28 11:44:35 +08:00
Ensure only RAW images in cache while keeping orginal image
Signed-off-by: Ansuman Sahoo <anshumansahoo500@gmail.com>
This commit is contained in:
14 files changed
+413
-44
No files matched your search
@@ -43,7 +43,7 @@ func EnsureNerdctlArchiveCache(ctx context.Context, y *limatype.LimaYAML, create
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
path, err := fileutils.DownloadFile(ctx, "", f, false, "the nerdctl archive", *y.Arch)
|
||||
path, err := fileutils.DownloadFile(ctx, "", f, false, "the nerdctl archive", *y.Arch, nil)
|
||||
if err != nil {
|
||||
errs[i] = err
|
||||
continue
|
||||
|
||||
+198
-13
@@ -16,16 +16,21 @@ import (
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
"github.com/containerd/continuity/fs"
|
||||
"github.com/lima-vm/go-qcow2reader/image/raw"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/lima-vm/lima/v2/pkg/httpclientutil"
|
||||
"github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil"
|
||||
"github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil"
|
||||
"github.com/lima-vm/lima/v2/pkg/iso9660util"
|
||||
"github.com/lima-vm/lima/v2/pkg/localpathutil"
|
||||
"github.com/lima-vm/lima/v2/pkg/lockutil"
|
||||
"github.com/lima-vm/lima/v2/pkg/progressbar"
|
||||
@@ -57,10 +62,11 @@ type Result struct {
|
||||
}
|
||||
|
||||
type options struct {
|
||||
cacheDir string // default: empty (disables caching)
|
||||
decompress bool // default: false (keep compression)
|
||||
description string // default: url
|
||||
expectedDigest digest.Digest
|
||||
cacheDir string // default: empty (disables caching)
|
||||
decompress bool // default: false (keep compression)
|
||||
description string // default: url
|
||||
expectedDigest digest.Digest
|
||||
supportedImageFormats []string
|
||||
}
|
||||
|
||||
func (o *options) apply(opts []Opt) error {
|
||||
@@ -111,6 +117,13 @@ func WithDecompress(decompress bool) Opt {
|
||||
}
|
||||
}
|
||||
|
||||
func WithImageFormats(supportedImageFormats []string) Opt {
|
||||
return func(o *options) error {
|
||||
o.supportedImageFormats = supportedImageFormats
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithExpectedDigest is used to validate the downloaded file against the expected digest.
|
||||
//
|
||||
// The digest is not verified in the following cases:
|
||||
@@ -267,6 +280,81 @@ func getCached(ctx context.Context, localPath, remote string, o options) (*Resul
|
||||
if _, err := os.Stat(shadData); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if _, err := os.Stat(shadDigest); err == nil {
|
||||
if err := validateCachedDigest(shadDigest, o.expectedDigest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if match, lmCached, lmRemote, err := matchLastModified(ctx, shadTime, remote); err != nil {
|
||||
logrus.WithError(err).Info("Failed to retrieve last-modified for cached digest-less image; using cached image.")
|
||||
} else if !match {
|
||||
logrus.Infof("Re-downloading digest-less image: last-modified mismatch (cached: %#q, remote: %#q)", lmCached, lmRemote)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Some drivers (e.g. vz) can only boot raw images. When the driver tells us
|
||||
// which formats it supports and the cached image is not one of them, we save
|
||||
// a converted raw copy next to the original at <shad>/imgconv/raw. See
|
||||
// website/content/en/docs/dev/internals.md for the layout.
|
||||
if len(o.supportedImageFormats) > 0 {
|
||||
isISO, err := iso9660util.IsISO9660(shadData)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Debugf("Skipping cache image conversion for %q (unable to check ISO9660)", shadData)
|
||||
} else if !isISO {
|
||||
imageFormat, err := nativeimgutil.DetectFormat(shadData)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Debugf("Skipping cache image conversion for %q (unable to detect format)", shadData)
|
||||
} else if !slices.Contains(o.supportedImageFormats, imageFormat) {
|
||||
rawImgConvPath := filepath.Join(shad, "imgconv", "raw")
|
||||
rawImgConvDigestPath := filepath.Join(shad, "imgconv", "raw.digest")
|
||||
|
||||
needConvert := false
|
||||
if rawStat, err := os.Stat(rawImgConvPath); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
needConvert = true
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
origStat, err := os.Stat(shadData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if origStat.ModTime().After(rawStat.ModTime()) {
|
||||
needConvert = true
|
||||
}
|
||||
}
|
||||
|
||||
if needConvert {
|
||||
logrus.Infof("Converted raw image is missing or stale; (re)converting now.")
|
||||
converted, rawDigest, err := ensureRawInCache(ctx, shadData, imageFormat, o.expectedDigest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shadData = converted
|
||||
if o.expectedDigest != "" {
|
||||
o.expectedDigest = rawDigest
|
||||
shadDigest = rawImgConvDigestPath
|
||||
}
|
||||
} else {
|
||||
shadData = rawImgConvPath
|
||||
if o.expectedDigest != "" {
|
||||
if currentDigestData, err := os.ReadFile(rawImgConvDigestPath); err == nil {
|
||||
currentDigest := strings.TrimSpace(string(currentDigestData))
|
||||
if d, err := digest.Parse(currentDigest); err == nil {
|
||||
o.expectedDigest = d
|
||||
shadDigest = rawImgConvDigestPath
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid digest in raw digest file %q: %w", rawImgConvDigestPath, err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to read raw digest file %q: %w", rawImgConvDigestPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ext := path.Ext(remote)
|
||||
logrus.Debugf("file %#q is cached as %#q", localPath, shadData)
|
||||
if _, err := os.Stat(shadDigest); err == nil {
|
||||
@@ -279,15 +367,8 @@ func getCached(ctx context.Context, localPath, remote string, o options) (*Resul
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if match, lmCached, lmRemote, err := matchLastModified(ctx, shadTime, remote); err != nil {
|
||||
logrus.WithError(err).Info("Failed to retrieve last-modified for cached digest-less image; using cached image.")
|
||||
} else if match {
|
||||
if err := copyLocal(ctx, localPath, shadData, ext, o.decompress, o.description, o.expectedDigest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
logrus.Infof("Re-downloading digest-less image: last-modified mismatch (cached: %#q, remote: %#q)", lmCached, lmRemote)
|
||||
return nil, nil
|
||||
if err := copyLocal(ctx, localPath, shadData, ext, o.decompress, o.description, o.expectedDigest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
res := &Result{
|
||||
@@ -323,6 +404,34 @@ func fetch(ctx context.Context, localPath, remote string, o options) (*Result, e
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If the driver cannot use the format we just downloaded, save a converted
|
||||
// raw copy at <shad>/imgconv/raw, keeping the original <shad>/data as-is.
|
||||
//
|
||||
// Gated on WithImageFormats() (the caller's signal that this is a VM disk
|
||||
// image) and on the file not being an ISO 9660 image.
|
||||
if len(o.supportedImageFormats) > 0 {
|
||||
isISO, err := iso9660util.IsISO9660(shadData)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Debugf("Skipping cache image conversion for %q (unable to check ISO9660)", shadData)
|
||||
} else if !isISO {
|
||||
format, err := nativeimgutil.DetectFormat(shadData)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Debugf("Skipping cache image conversion for %q (unable to detect format)", shadData)
|
||||
} else if !slices.Contains(o.supportedImageFormats, format) {
|
||||
converted, rawDigest, err := ensureRawInCache(ctx, shadData, format, o.expectedDigest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert image to raw: %w", err)
|
||||
} else if converted != "" {
|
||||
shadData = converted
|
||||
if o.expectedDigest != "" {
|
||||
o.expectedDigest = rawDigest
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no need to pass the digest to copyLocal(), as we already verified the digest
|
||||
if err := copyLocal(ctx, localPath, shadData, ext, o.decompress, "", ""); err != nil {
|
||||
return nil, err
|
||||
@@ -337,6 +446,82 @@ func fetch(ctx context.Context, localPath, remote string, o options) (*Result, e
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ensureRawInCache converts any image to raw and places it in the cache(imgconv/raw). It also creates a
|
||||
// digest file for the raw image(imgconv/raw.digest). Returns the converted image path, the raw digest, and any error.
|
||||
func ensureRawInCache(ctx context.Context, imagePath, format string, originalDigest digest.Digest) (string, digest.Digest, error) {
|
||||
imgConvPath := filepath.Join(filepath.Dir(imagePath), "imgconv")
|
||||
if err := os.MkdirAll(imgConvPath, 0o700); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
rawImgConvPath := filepath.Join(imgConvPath, "raw")
|
||||
|
||||
logrus.Infof("Converting %s image to raw sparse format in cache: %q", format, rawImgConvPath)
|
||||
rawPathTmp := filepath.Join(imgConvPath, "raw.tmp")
|
||||
defer os.Remove(rawPathTmp)
|
||||
diskUtil := proxyimgutil.NewDiskUtil(ctx)
|
||||
if err := diskUtil.Convert(ctx, raw.Type, imagePath, rawPathTmp, nil, false); err != nil {
|
||||
return "", "", fmt.Errorf("failed to convert %q to raw: %w", imagePath, err)
|
||||
}
|
||||
|
||||
// Ensure the image is sparse to save cache space.
|
||||
rawTmpF, err := os.OpenFile(rawPathTmp, os.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to open raw tmp file %q: %w", rawPathTmp, err)
|
||||
}
|
||||
fi, err := rawTmpF.Stat()
|
||||
if err != nil {
|
||||
_ = rawTmpF.Close()
|
||||
return "", "", fmt.Errorf("failed to stat raw tmp file %q: %w", rawPathTmp, err)
|
||||
}
|
||||
if err := diskUtil.MakeSparse(ctx, rawTmpF, fi.Size()); err != nil {
|
||||
logrus.WithError(err).Warnf("Failed to make %q sparse (non-fatal)", rawPathTmp)
|
||||
}
|
||||
if err := rawTmpF.Close(); err != nil {
|
||||
return "", "", fmt.Errorf("failed to close raw tmp file %q: %w", rawPathTmp, err)
|
||||
}
|
||||
|
||||
if err := os.Remove(rawImgConvPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return "", "", fmt.Errorf("failed to remove stale raw image %q: %w", rawImgConvPath, err)
|
||||
}
|
||||
if err := os.Rename(rawPathTmp, rawImgConvPath); err != nil {
|
||||
return "", "", fmt.Errorf("failed to replace original with raw image: %w", err)
|
||||
}
|
||||
|
||||
algo := digest.Canonical
|
||||
if originalDigest != "" {
|
||||
algo = originalDigest.Algorithm()
|
||||
}
|
||||
rawDigest, err := calculateFileDigest(rawImgConvPath, algo)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to calculate digest of raw image: %w", err)
|
||||
}
|
||||
|
||||
rawDigestPath := filepath.Join(imgConvPath, "raw.digest")
|
||||
rawDigestPathTmp := rawDigestPath + ".tmp"
|
||||
defer os.Remove(rawDigestPathTmp)
|
||||
if err := os.WriteFile(rawDigestPathTmp, []byte(rawDigest.String()), 0o644); err != nil {
|
||||
return "", "", fmt.Errorf("failed to write raw digest file: %w", err)
|
||||
}
|
||||
if err := os.Remove(rawDigestPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return "", "", fmt.Errorf("failed to remove stale raw digest file %q: %w", rawDigestPath, err)
|
||||
}
|
||||
if err := os.Rename(rawDigestPathTmp, rawDigestPath); err != nil {
|
||||
return "", "", fmt.Errorf("failed to rename raw digest file: %w", err)
|
||||
}
|
||||
|
||||
return rawImgConvPath, rawDigest, nil
|
||||
}
|
||||
|
||||
func calculateFileDigest(path string, algo digest.Algorithm) (digest.Digest, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return algo.FromReader(f)
|
||||
}
|
||||
|
||||
// Cached checks if the remote resource is in the cache.
|
||||
//
|
||||
// Download caches the remote resource if WithCache or WithCacheDir option is specified.
|
||||
|
||||
@@ -398,3 +398,137 @@ func TestDownloadCompressed(t *testing.T) {
|
||||
assert.Equal(t, string(got), string(testDownloadCompressedContents))
|
||||
})
|
||||
}
|
||||
|
||||
// This test simulates the end-to-end flow of downloading a remote image and converting it.
|
||||
func TestDownloadImageConversion(t *testing.T) {
|
||||
_, err := exec.LookPath("qemu-img")
|
||||
if err != nil {
|
||||
t.Skipf("qemu-img does not seem installed: %v", err)
|
||||
}
|
||||
|
||||
remoteDir := t.TempDir()
|
||||
ts := httptest.NewServer(http.FileServer(http.Dir(remoteDir)))
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
qcow2Path := filepath.Join(remoteDir, "test.qcow2")
|
||||
assert.NilError(t, exec.CommandContext(t.Context(), "qemu-img", "create", "-f", "qcow2", qcow2Path, "64K").Run())
|
||||
|
||||
t.Run("without-digest", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
cacheDir := t.TempDir()
|
||||
localPath := filepath.Join(t.TempDir(), "local")
|
||||
remoteURL := ts.URL + "/test.qcow2"
|
||||
|
||||
// 1. First download, should convert to raw
|
||||
r, err := Download(ctx, localPath, remoteURL,
|
||||
WithCacheDir(cacheDir),
|
||||
WithDescription("image"),
|
||||
WithDecompress(true),
|
||||
WithImageFormats([]string{"raw"}),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, StatusDownloaded, r.Status)
|
||||
|
||||
shad := cacheDirectoryPath(cacheDir, remoteURL)
|
||||
rawPath := filepath.Join(shad, "imgconv", "raw")
|
||||
_, err = os.Stat(rawPath)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// 2. Second download, should use cached raw
|
||||
localPath2 := filepath.Join(t.TempDir(), "local2")
|
||||
r, err = Download(ctx, localPath2, remoteURL,
|
||||
WithCacheDir(cacheDir),
|
||||
WithDescription("image"),
|
||||
WithDecompress(true),
|
||||
WithImageFormats([]string{"raw"}),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, StatusUsedCache, r.Status)
|
||||
assert.Equal(t, rawPath, r.CachePath)
|
||||
})
|
||||
|
||||
t.Run("with-digest", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
cacheDir := t.TempDir()
|
||||
localPath := filepath.Join(t.TempDir(), "local")
|
||||
remoteURL := ts.URL + "/test.qcow2"
|
||||
|
||||
content, err := os.ReadFile(qcow2Path)
|
||||
assert.NilError(t, err)
|
||||
originalDigest := digest.SHA256.FromBytes(content)
|
||||
|
||||
// 1. First download, should convert to raw
|
||||
r, err := Download(ctx, localPath, remoteURL,
|
||||
WithCacheDir(cacheDir),
|
||||
WithDescription("image"),
|
||||
WithDecompress(true),
|
||||
WithImageFormats([]string{"raw"}),
|
||||
WithExpectedDigest(originalDigest),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, StatusDownloaded, r.Status)
|
||||
|
||||
shad := cacheDirectoryPath(cacheDir, remoteURL)
|
||||
rawPath := filepath.Join(shad, "imgconv", "raw")
|
||||
_, err = os.Stat(rawPath)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// 2. Second download, should use cached raw
|
||||
localPath2 := filepath.Join(t.TempDir(), "local2")
|
||||
r, err = Download(ctx, localPath2, remoteURL,
|
||||
WithCacheDir(cacheDir),
|
||||
WithDescription("image"),
|
||||
WithDecompress(true),
|
||||
WithImageFormats([]string{"raw"}),
|
||||
WithExpectedDigest(originalDigest),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, StatusUsedCache, r.Status)
|
||||
assert.Equal(t, rawPath, r.CachePath)
|
||||
})
|
||||
}
|
||||
|
||||
// This test focuses specifically on the scenario where the source image
|
||||
// is already in the Lima cache but the converted version does not exist yet.
|
||||
func TestDownloadImageConversionCached(t *testing.T) {
|
||||
_, err := exec.LookPath("qemu-img")
|
||||
if err != nil {
|
||||
t.Skipf("qemu-img does not seem installed: %v", err)
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
tmpDir := t.TempDir()
|
||||
cacheDir := filepath.Join(tmpDir, "cache")
|
||||
remoteURL := "https://example.com/test.qcow2"
|
||||
|
||||
// Pre-populate cache with a qcow2 file
|
||||
shad := cacheDirectoryPath(cacheDir, remoteURL)
|
||||
assert.NilError(t, os.MkdirAll(shad, 0o700))
|
||||
shadData := filepath.Join(shad, "data")
|
||||
assert.NilError(t, exec.CommandContext(t.Context(), "qemu-img", "create", "-f", "qcow2", shadData, "64K").Run())
|
||||
|
||||
// Provide a cached digest to avoid HTTP HEAD / re-download.
|
||||
content, err := os.ReadFile(shadData)
|
||||
assert.NilError(t, err)
|
||||
originalDigest := digest.SHA256.FromBytes(content)
|
||||
shadDigest := filepath.Join(shad, "sha256.digest")
|
||||
assert.NilError(t, os.WriteFile(shadDigest, []byte(originalDigest.String()), 0o644))
|
||||
|
||||
localPath := filepath.Join(tmpDir, "local")
|
||||
|
||||
// Call Download, it should find qcow2 in cache, see it's not supported, and convert it
|
||||
r, err := Download(ctx, localPath, remoteURL,
|
||||
WithCacheDir(cacheDir),
|
||||
WithDescription("image"),
|
||||
WithImageFormats([]string{"raw"}),
|
||||
WithExpectedDigest(originalDigest),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, StatusUsedCache, r.Status)
|
||||
|
||||
// Verify conversion happened
|
||||
rawPath := filepath.Join(shad, "imgconv", "raw")
|
||||
_, err = os.Stat(rawPath)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, rawPath, r.CachePath)
|
||||
}
|
||||
@@ -129,11 +129,12 @@ type Info struct {
|
||||
}
|
||||
|
||||
type DriverFeatures struct {
|
||||
CanRunGUI bool `json:"canRunGui,omitempty"`
|
||||
DynamicSSHAddress bool `json:"dynamicSSHAddress"`
|
||||
StaticSSHPort bool `json:"staticSSHPort"`
|
||||
SkipSocketForwarding bool `json:"skipSocketForwarding"`
|
||||
NoCloudInit bool `json:"noCloudInit"`
|
||||
RosettaEnabled bool `json:"rosettaEnabled"`
|
||||
RosettaBinFmt bool `json:"rosettaBinFmt"`
|
||||
CanRunGUI bool `json:"canRunGui,omitempty"`
|
||||
DynamicSSHAddress bool `json:"dynamicSSHAddress"`
|
||||
StaticSSHPort bool `json:"staticSSHPort"`
|
||||
SkipSocketForwarding bool `json:"skipSocketForwarding"`
|
||||
NoCloudInit bool `json:"noCloudInit"`
|
||||
RosettaEnabled bool `json:"rosettaEnabled"`
|
||||
RosettaBinFmt bool `json:"rosettaBinFmt"`
|
||||
SupportedImageFormats []string `json:"supportedImageFormats,omitempty"`
|
||||
}
|
||||
@@ -335,9 +335,10 @@ func (l *LimaKrunkitDriver) Info(_ context.Context) driver.Info {
|
||||
}
|
||||
|
||||
info.Features = driver.DriverFeatures{
|
||||
DynamicSSHAddress: false,
|
||||
SkipSocketForwarding: false,
|
||||
CanRunGUI: false,
|
||||
DynamicSSHAddress: false,
|
||||
SkipSocketForwarding: false,
|
||||
CanRunGUI: false,
|
||||
SupportedImageFormats: []string{string(raw.Type)},
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -610,7 +610,7 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
|
||||
switch f.VMType {
|
||||
case "", limatype.QEMU:
|
||||
if f.Arch == *y.Arch {
|
||||
if _, err = fileutils.DownloadFile(ctx, downloadedFirmware, f.File, true, "UEFI code "+f.Location, *y.Arch); err != nil {
|
||||
if _, err = fileutils.DownloadFile(ctx, downloadedFirmware, f.File, true, "UEFI code "+f.Location, *y.Arch, nil); err != nil {
|
||||
logrus.WithError(err).Warnf("failed to download %#q", f.Location)
|
||||
continue loop
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ import (
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"github.com/digitalocean/go-qemu/qmp"
|
||||
"github.com/digitalocean/go-qemu/qmp/raw"
|
||||
"github.com/lima-vm/go-qcow2reader/image/qcow2"
|
||||
rawImage "github.com/lima-vm/go-qcow2reader/image/raw"
|
||||
"github.com/lima-vm/go-qcow2reader/image/vhdx"
|
||||
"github.com/lima-vm/go-qcow2reader/image/vmdk"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/lima-vm/lima/v2/pkg/driver"
|
||||
@@ -866,9 +870,10 @@ func (l *LimaQemuDriver) Info(_ context.Context) driver.Info {
|
||||
info.VsockPort = l.vSockPort
|
||||
|
||||
info.Features = driver.DriverFeatures{
|
||||
DynamicSSHAddress: false,
|
||||
SkipSocketForwarding: false,
|
||||
CanRunGUI: false,
|
||||
DynamicSSHAddress: false,
|
||||
SkipSocketForwarding: false,
|
||||
CanRunGUI: false,
|
||||
SupportedImageFormats: []string{string(qcow2.Type), string(rawImage.Type), string(vmdk.Type), string(vhdx.Type)}, // Not a comprehensive list of supported formats
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -559,11 +559,12 @@ func (l *LimaVzDriver) Info(_ context.Context) driver.Info {
|
||||
guiFlag = l.canRunGUI()
|
||||
}
|
||||
info.Features = driver.DriverFeatures{
|
||||
DynamicSSHAddress: false,
|
||||
SkipSocketForwarding: false,
|
||||
CanRunGUI: guiFlag,
|
||||
RosettaEnabled: l.rosettaEnabled,
|
||||
RosettaBinFmt: l.rosettaBinFmt,
|
||||
DynamicSSHAddress: false,
|
||||
SkipSocketForwarding: false,
|
||||
CanRunGUI: guiFlag,
|
||||
RosettaEnabled: l.rosettaEnabled,
|
||||
RosettaBinFmt: l.rosettaBinFmt,
|
||||
SupportedImageFormats: []string{string(raw.Type), string(asif.Type)},
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func EnsureFs(ctx context.Context, inst *limatype.Instance) error {
|
||||
var ensuredBaseDisk bool
|
||||
errs := make([]error, len(inst.Config.Images))
|
||||
for i, f := range inst.Config.Images {
|
||||
if _, err := fileutils.DownloadFile(ctx, baseDisk, f.File, true, "the image", *inst.Config.Arch); err != nil {
|
||||
if _, err := fileutils.DownloadFile(ctx, baseDisk, f.File, true, "the image", *inst.Config.Arch, nil); err != nil {
|
||||
errs[i] = err
|
||||
continue
|
||||
}
|
||||
|
||||
+19
-3
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/lima-vm/go-qcow2reader/image"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil"
|
||||
"github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil"
|
||||
"github.com/lima-vm/lima/v2/pkg/iso9660util"
|
||||
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
|
||||
@@ -95,10 +96,25 @@ func EnsureDisk(ctx context.Context, instDir, diskSize string, diskImageFormat i
|
||||
return fmt.Errorf("failed to create disk %#q: %w", diskPath, err)
|
||||
}
|
||||
} else {
|
||||
if err := diskUtil.Convert(ctx, diskImageFormat, imagePath, diskPath, &diskSizeInBytes, false); err != nil {
|
||||
return fmt.Errorf("failed to convert %#q to %#q: %w", imagePath, diskPath, err)
|
||||
format, err := nativeimgutil.DetectFormat(imagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resize handled by prepareDisk() after CreateDisk()
|
||||
if format == string(diskImageFormat) {
|
||||
if err = os.Rename(imagePath, diskPath); err != nil {
|
||||
return fmt.Errorf("failed to rename %#q to %#q: %w", imagePath, diskPath, err)
|
||||
}
|
||||
if err := os.Chmod(diskPath, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to chmod %#q: %w", diskPath, err)
|
||||
}
|
||||
} else {
|
||||
if err := diskUtil.Convert(ctx, diskImageFormat, imagePath, diskPath, &diskSizeInBytes, false); err != nil {
|
||||
return fmt.Errorf("failed to convert %#q to %#q: %w", imagePath, diskPath, err)
|
||||
}
|
||||
os.Remove(imagePath)
|
||||
}
|
||||
os.Remove(imagePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -19,18 +19,23 @@ import (
|
||||
var ErrSkipped = errors.New("skipped to download")
|
||||
|
||||
// DownloadFile downloads a file to the cache, optionally copying it to the destination. Returns path in cache.
|
||||
func DownloadFile(ctx context.Context, dest string, f limatype.File, decompress bool, description string, expectedArch limatype.Arch) (string, error) {
|
||||
func DownloadFile(ctx context.Context, dest string, f limatype.File, decompress bool, description string, expectedArch limatype.Arch, supportedImageFormats []string) (string, error) {
|
||||
if f.Arch != expectedArch {
|
||||
return "", fmt.Errorf("%w: %#q: unsupported arch: %#q", ErrSkipped, f.Location, f.Arch)
|
||||
}
|
||||
fields := logrus.Fields{"location": f.Location, "arch": f.Arch, "digest": f.Digest}
|
||||
logrus.WithFields(fields).Infof("Attempting to download %s", description)
|
||||
res, err := downloader.Download(ctx, dest, f.Location,
|
||||
opts := []downloader.Opt{
|
||||
downloader.WithCache(),
|
||||
downloader.WithDecompress(decompress),
|
||||
downloader.WithDescription(fmt.Sprintf("%s (%s)", description, path.Base(f.Location))),
|
||||
downloader.WithExpectedDigest(f.Digest),
|
||||
)
|
||||
}
|
||||
if len(supportedImageFormats) > 0 {
|
||||
opts = append(opts, downloader.WithImageFormats(supportedImageFormats))
|
||||
}
|
||||
|
||||
res, err := downloader.Download(ctx, dest, f.Location, opts...)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download %#q: %w", f.Location, err)
|
||||
}
|
||||
|
||||
@@ -216,6 +216,22 @@ func makeSparse(f *os.File, offset int64) error {
|
||||
return f.Truncate(offset)
|
||||
}
|
||||
|
||||
func DetectFormat(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
img, err := qcow2reader.Open(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer img.Close()
|
||||
|
||||
return string(img.Type()), nil
|
||||
}
|
||||
|
||||
// CreateDisk creates a new disk image with the specified size.
|
||||
func (n *NativeImageUtil) CreateDisk(_ context.Context, disk string, size int64) error {
|
||||
if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
|
||||
|
||||
@@ -84,6 +84,8 @@ func Prepare(ctx context.Context, inst *limatype.Instance, guestAgent string) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supportedImageFormats := limaDriver.Info(ctx).Features.SupportedImageFormats
|
||||
|
||||
created := limayaml.IsExistingInstanceDir(inst.Dir)
|
||||
|
||||
imagePath := filepath.Join(inst.Dir, filenames.Image)
|
||||
@@ -112,13 +114,13 @@ func Prepare(ctx context.Context, inst *limatype.Instance, guestAgent string) (*
|
||||
errs[i] = fmt.Errorf("%w: variant mismatch (expected %q, got %q)", fileutils.ErrSkipped, targetVariant, f.Variant)
|
||||
continue
|
||||
}
|
||||
if _, err := fileutils.DownloadFile(ctx, imagePath, f.File, true, "the image", *inst.Config.Arch); err != nil {
|
||||
if _, err := fileutils.DownloadFile(ctx, imagePath, f.File, true, "the image", *inst.Config.Arch, supportedImageFormats); err != nil {
|
||||
errs[i] = err
|
||||
continue
|
||||
}
|
||||
if f.Kernel != nil {
|
||||
// ensure decompress kernel because vz expects it to be decompressed
|
||||
if _, err := fileutils.DownloadFile(ctx, kernel, f.Kernel.File, true, "the kernel", *inst.Config.Arch); err != nil {
|
||||
if _, err := fileutils.DownloadFile(ctx, kernel, f.Kernel.File, true, "the kernel", *inst.Config.Arch, nil); err != nil {
|
||||
errs[i] = err
|
||||
continue
|
||||
}
|
||||
@@ -131,7 +133,7 @@ func Prepare(ctx context.Context, inst *limatype.Instance, guestAgent string) (*
|
||||
}
|
||||
if f.Initrd != nil {
|
||||
// vz does not need initrd to be decompressed
|
||||
if _, err := fileutils.DownloadFile(ctx, initrd, *f.Initrd, false, "the initrd", *inst.Config.Arch); err != nil {
|
||||
if _, err := fileutils.DownloadFile(ctx, initrd, *f.Initrd, false, "the initrd", *inst.Config.Arch, nil); err != nil {
|
||||
errs[i] = err
|
||||
continue
|
||||
}
|
||||
@@ -155,7 +157,7 @@ func Prepare(ctx context.Context, inst *limatype.Instance, guestAgent string) (*
|
||||
errs := make([]error, len(winOpts.VirtioWin))
|
||||
virtioWin := filepath.Join(inst.Dir, filenames.VirtioWin)
|
||||
for i, f := range winOpts.VirtioWin {
|
||||
if _, err := fileutils.DownloadFile(ctx, virtioWin, f, true, "virtio-win", *inst.Config.Arch); err != nil {
|
||||
if _, err := fileutils.DownloadFile(ctx, virtioWin, f, true, "virtio-win", *inst.Config.Arch, nil); err != nil {
|
||||
errs[i] = err
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -145,6 +145,9 @@ The directory contains the following files:
|
||||
- `data`: data
|
||||
- `<ALGO>.digest`: digest of the data, in OCI format.
|
||||
e.g., file name `sha256.digest`, with content `sha256:5ba3d476707d510fe3ca3928e9cda5d0b4ce527d42b343404c92d563f82ba967`
|
||||
- `imgconv/`: (Optional) a converted copy of the image, in a format the current driver can use. The downloader creates this on demand when the driver reports its `SupportedImageFormats` and the cached `data` is not one of them (for example, a `qcow2` image requested by the `vz` driver, which only boots raw). The original `data` and its digest file are left as-is, so another driver can still use them.
|
||||
- `raw`: the converted raw image. If `data` is newer than `raw`, the raw copy is treated as stale and rebuilt.
|
||||
- `raw.digest`: digest of the raw image. Uses the same algorithm as the digest the caller passed in, and is only read back when the caller passes an expected digest.
|
||||
|
||||
|
||||
## Ansible
|
||||
|
||||
Reference in new issue
Block a user