Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: Allow loading images by name from imagestore #2404

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions .ls-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ ls:
docs:
.md: kebab-case

images:
# valid names are `ubuntu-24.04` or `debian-12`
.yaml: regex:[a-z0-9-.]+

website/content:
.dir: lowercase

Expand Down
18 changes: 17 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,14 @@ help-targets:
@echo '- default_template : Copy default.yaml template'
@echo '- create-examples-link : Create a symlink at ../examples pointing to templates'
@echo
@echo 'Targets for files in _output/share/lima/images/:'
@echo '- images : Copy images'
@echo
@echo 'Targets for files in _output/share/doc/lima:'
@echo '- documentation : Copy documentation to _output/share/doc/lima'
@echo '- create-links-in-doc-dir : Create some symlinks pointing ../../lima/templates'
@echo
@echo '# e.g. to install limactl, helpers, native guestagent, and templates:'
@echo '# e.g. to install limactl, helpers, native guestagent, images and templates:'
@echo '# make native install'

.PHONY: help-artifact
Expand Down Expand Up @@ -312,6 +315,19 @@ _output/share/lima/templates/%: examples/%
# On Windows, always copy to ensure the target has the same file as the source.
force_link = $(if $(filter windows,$(GOOS)),force,$(shell test ! -L $(1) && echo force))

################################################################################
# _output/share/lima/images
IMAGES = $(addprefix _output/share/lima/images/,$(notdir $(wildcard images/*)))

.PHONY: images
images: $(IMAGES)

$(IMAGES): | _output/share/lima/images
MKDIR_TARGETS += _output/share/lima/images

_output/share/lima/images/%: images/%
cp -aL $< $@

################################################################################
# _output/share/lima/examples
.PHONY: create-examples-link
Expand Down
4 changes: 4 additions & 0 deletions cmd/limactl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"strings"

"github.com/lima-vm/lima/pkg/fsutil"
"github.com/lima-vm/lima/pkg/imagestore"
"github.com/lima-vm/lima/pkg/limayaml"
"github.com/lima-vm/lima/pkg/osutil"
"github.com/lima-vm/lima/pkg/store/dirnames"
"github.com/lima-vm/lima/pkg/version"
Expand Down Expand Up @@ -111,6 +113,8 @@ func newApp() *cobra.Command {
if err != nil {
return err
}
// Connect limayaml.Load to imagestore
limayaml.ReadImage = imagestore.Read
// Make sure that directory is on a local filesystem, not on NFS
// if the directory does not yet exist, check the home directory
_, err = os.Stat(dir)
Expand Down
16 changes: 2 additions & 14 deletions examples/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,9 @@ arch: null

# OpenStack-compatible disk image.
# 🟢 Builtin default: null (must be specified)
# 🔵 This file: Ubuntu images
# 🔵 This file: ["default"] (see the output of `limactl info | jq .defaultImage.images`)
images:
# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.
- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-amd64.img"
arch: "x86_64"
digest: "sha256:0e25ca6ee9f08ec5d4f9910054b66ae7163c6152e81a3e67689d89bd6e4dfa69"
- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-arm64.img"
arch: "aarch64"
digest: "sha256:5ecac6447be66a164626744a87a27fd4e6c6606dc683e0a233870af63df4276a"
# Fallback to the latest release image.
# Hint: run `limactl prune` to invalidate the cache
- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
arch: "x86_64"
- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
arch: "aarch64"
- default

# CPUs
# 🟢 Builtin default: min(4, host CPU cores)
Expand Down
14 changes: 14 additions & 0 deletions images/ubuntu-24.04.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
images:
# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.
- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-amd64.img"
arch: "x86_64"
digest: "sha256:0e25ca6ee9f08ec5d4f9910054b66ae7163c6152e81a3e67689d89bd6e4dfa69"
- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-arm64.img"
arch: "aarch64"
digest: "sha256:5ecac6447be66a164626744a87a27fd4e6c6606dc683e0a233870af63df4276a"
# Fallback to the latest release image.
# Hint: run `limactl prune` to invalidate the cache
- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
arch: "x86_64"
- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
arch: "aarch64"
63 changes: 63 additions & 0 deletions pkg/imagestore/imagestore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package imagestore

import (
"io/fs"
"os"
"path/filepath"
"strings"

securejoin "github.com/cyphar/filepath-securejoin"
"github.com/lima-vm/lima/pkg/usrlocalsharelima"
)

type Image struct {
Name string `json:"name"`
Location string `json:"location"`
}

func Read(name string) ([]byte, error) {
dir, err := usrlocalsharelima.Dir()
if err != nil {
return nil, err
}
if name == "default" {
name = Default
}
yamlPath, err := securejoin.SecureJoin(filepath.Join(dir, "images"), name+".yaml")
if err != nil {
return nil, err
}
return os.ReadFile(yamlPath)
}

const Default = "ubuntu-24.04"

func Images() ([]Image, error) {
usrlocalsharelimaDir, err := usrlocalsharelima.Dir()
if err != nil {
return nil, err
}
imagesDir := filepath.Join(usrlocalsharelimaDir, "images")

var res []Image
walkDirFn := func(p string, _ fs.DirEntry, err error) error {
if err != nil {
return err
}
base := filepath.Base(p)
if strings.HasPrefix(base, ".") || !strings.HasSuffix(base, ".yaml") {
return nil
}
x := Image{
// Name is like "ubuntu-24.04", "debian-12", ...
Name: strings.TrimSuffix(strings.TrimPrefix(p, imagesDir+"/"), ".yaml"),
Location: p,
}
res = append(res, x)
return nil
}
if err = filepath.WalkDir(imagesDir, walkDirFn); err != nil {
return nil, err
}
return res, nil
}
16 changes: 16 additions & 0 deletions pkg/infoutil/infoutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package infoutil

import (
"github.com/lima-vm/lima/pkg/driverutil"
"github.com/lima-vm/lima/pkg/imagestore"
"github.com/lima-vm/lima/pkg/limayaml"
"github.com/lima-vm/lima/pkg/store/dirnames"
"github.com/lima-vm/lima/pkg/templatestore"
Expand All @@ -12,6 +13,8 @@ type Info struct {
Version string `json:"version"`
Templates []templatestore.Template `json:"templates"`
DefaultTemplate *limayaml.LimaYAML `json:"defaultTemplate"`
Images []imagestore.Image `json:"images"`
DefaultImage *limayaml.ImageYAML `json:"defaultImage"`
LimaHome string `json:"limaHome"`
VMTypes []string `json:"vmTypes"` // since Lima v0.14.2
}
Expand All @@ -25,15 +28,28 @@ func GetInfo() (*Info, error) {
if err != nil {
return nil, err
}
bi, err := imagestore.Read(imagestore.Default)
if err != nil {
return nil, err
}
yi, err := limayaml.LoadImage(bi, "")
if err != nil {
return nil, err
}
info := &Info{
Version: version.Version,
DefaultTemplate: y,
DefaultImage: yi,
VMTypes: driverutil.Drivers(),
}
info.Templates, err = templatestore.Templates()
if err != nil {
return nil, err
}
info.Images, err = imagestore.Images()
if err != nil {
return nil, err
}
info.LimaHome, err = dirnames.LimaDir()
if err != nil {
return nil, err
Expand Down
45 changes: 45 additions & 0 deletions pkg/limayaml/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ func defaultContainerdArchives() []File {
return containerd.Archives
}

func defaultMounts() []Mount {
return []Mount{
{
Location: "~",
Writable: ptr.Of(false),
},
{
Location: "/tmp/lima",
Writable: ptr.Of(true),
},
}
}

// FirstUsernetIndex gets the index of first usernet network under l.Network[]. Returns -1 if no usernet network found.
func FirstUsernetIndex(l *LimaYAML) int {
return slices.IndexFunc(l.Networks, func(network Network) bool { return networks.IsUsernet(network.Lima) })
Expand Down Expand Up @@ -155,6 +168,8 @@ func defaultGuestInstallPrefix() string {
return "/usr/local"
}

var ReadImage func(name string) ([]byte, error)

// FillDefault updates undefined fields in y with defaults from d (or built-in default), and overwrites with values from o.
// Both d and o may be empty.
//
Expand Down Expand Up @@ -194,6 +209,26 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
y.Arch = ptr.Of(ResolveArch(y.Arch))

y.Images = append(append(o.Images, y.Images...), d.Images...)
images := []Image{}
for i := range y.Images {
img := &y.Images[i]
if img.Name != "" && ReadImage != nil {
ib, err := ReadImage(img.Name)
if err != nil {
logrus.Error(err)
continue
}
iy, err := LoadImage(ib, img.Name)
if err != nil {
logrus.Error(err)
continue
}
images = append(images, iy.Images...)
} else {
images = append(images, *img)
}
}
y.Images = images
for i := range y.Images {
img := &y.Images[i]
if img.Arch == "" {
Expand Down Expand Up @@ -625,6 +660,16 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
}
y.Mounts = mounts

mounts = []Mount{}
for _, mount := range y.Mounts {
if mount.Name == "default" {
mounts = append(mounts, defaultMounts()...)
continue
}
mounts = append(mounts, mount)
}
y.Mounts = mounts

for i := range y.Mounts {
mount := &y.Mounts[i]
if mount.SSHFS.Cache == nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/limayaml/image.yaml
6 changes: 6 additions & 0 deletions pkg/limayaml/limayaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ type LimaYAML struct {
TimeZone *string `yaml:"timezone,omitempty" json:"timezone,omitempty"`
}

type ImageYAML struct {
Images []Image `yaml:"images" json:"images"`
}

type (
OS = string
Arch = string
Expand Down Expand Up @@ -104,6 +108,7 @@ type Kernel struct {
}

type Image struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
File `yaml:",inline"`
Kernel *Kernel `yaml:"kernel,omitempty" json:"kernel,omitempty"`
Initrd *File `yaml:"initrd,omitempty" json:"initrd,omitempty"`
Expand All @@ -117,6 +122,7 @@ type Disk struct {
}

type Mount struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Location string `yaml:"location" json:"location"` // REQUIRED
MountPoint string `yaml:"mountPoint,omitempty" json:"mountPoint,omitempty"`
Writable *bool `yaml:"writable,omitempty" json:"writable,omitempty"`
Expand Down
37 changes: 35 additions & 2 deletions pkg/limayaml/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ import (
"github.com/sirupsen/logrus"
)

func unmarshalMount(dst *Mount, b []byte) error {
var s string
if err := yaml.Unmarshal(b, &s); err == nil {
*dst = Mount{Name: s}
return nil
}
return yaml.Unmarshal(b, dst)
}

func unmarshalDisk(dst *Disk, b []byte) error {
var s string
if err := yaml.Unmarshal(b, &s); err == nil {
Expand All @@ -22,23 +31,47 @@ func unmarshalDisk(dst *Disk, b []byte) error {
return yaml.Unmarshal(b, dst)
}

func unmarshalImage(dst *Image, b []byte) error {
var s string
if err := yaml.Unmarshal(b, &s); err == nil {
*dst = Image{Name: s}
return nil
}
return yaml.Unmarshal(b, dst)
}

var customMarshalers = []yaml.DecodeOption{
yaml.CustomUnmarshaler[Mount](unmarshalMount),
yaml.CustomUnmarshaler[Disk](unmarshalDisk),
yaml.CustomUnmarshaler[Image](unmarshalImage),
}

func unmarshalYAML(data []byte, v interface{}, comment string) error {
if err := yaml.UnmarshalWithOptions(data, v, yaml.DisallowDuplicateKey(), yaml.CustomUnmarshaler[Disk](unmarshalDisk)); err != nil {
if err := yaml.UnmarshalWithOptions(data, v, append(customMarshalers, yaml.DisallowDuplicateKey())...); err != nil {
return fmt.Errorf("failed to unmarshal YAML (%s): %w", comment, err)
}
// the go-yaml library doesn't catch all markup errors, unfortunately
// make sure to get a "second opinion", using the same library as "yq"
if err := yqutil.ValidateContent(data); err != nil {
return fmt.Errorf("failed to unmarshal YAML (%s): %w", comment, err)
}
if err := yaml.UnmarshalWithOptions(data, v, yaml.Strict(), yaml.CustomUnmarshaler[Disk](unmarshalDisk)); err != nil {
if err := yaml.UnmarshalWithOptions(data, v, append(customMarshalers, yaml.Strict())...); err != nil {
logrus.WithField("comment", comment).WithError(err).Warn("Non-strict YAML is deprecated and will be unsupported in a future version of Lima")
// Non-strict YAML is known to be used by Rancher Desktop:
// https://github.com/rancher-sandbox/rancher-desktop/blob/c7ea7508a0191634adf16f4675f64c73198e8d37/src/backend/lima.ts#L114-L117
}
return nil
}

// LoadImage loads the yaml.
func LoadImage(b []byte, filePath string) (*ImageYAML, error) {
var y ImageYAML
if err := unmarshalYAML(b, &y, filePath); err != nil {
return nil, err
}
return &y, nil
}

// Load loads the yaml and fulfills unspecified fields with the default values.
//
// Load does not validate. Use Validate for validation.
Expand Down
Loading
Loading