Skip to content

Commit

Permalink
Allow loading images by name from imagestore
Browse files Browse the repository at this point in the history
Signed-off-by: Anders F Björklund <anders.f.bjorklund@gmail.com>
  • Loading branch information
afbjorklund committed Aug 31, 2024
1 parent 726f61d commit 38522fa
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 16 deletions.
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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ binaries: clean \
$(HELPERS) \
$(GUESTAGENT)
cp -aL examples _output/share/lima/templates
cp -aL images _output/share/lima/images
ifneq ($(GOOS),windows)
ln -sf templates _output/share/lima/examples
else
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 @@ -97,6 +99,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 @@ -21,21 +21,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-20240423/ubuntu-24.04-server-cloudimg-amd64.img"
arch: "x86_64"
digest: "sha256:32a9d30d18803da72f5936cf2b7b9efcb4d0bb63c67933f17e3bdfd1751de3f3"
- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240423/ubuntu-24.04-server-cloudimg-arm64.img"
arch: "aarch64"
digest: "sha256:c841bac00925d3e6892d979798103a867931f255f28fefd9d5e07e3e22d0ef22"
# 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
22 changes: 22 additions & 0 deletions pkg/limayaml/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,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 @@ -197,6 +199,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
1 change: 1 addition & 0 deletions pkg/limayaml/image.yaml
5 changes: 5 additions & 0 deletions pkg/limayaml/limayaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,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 @@ -94,6 +98,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 Down
38 changes: 36 additions & 2 deletions pkg/limayaml/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ 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[Disk](unmarshalDisk),
yaml.CustomUnmarshaler[Image](unmarshalImage),
}

func (d *Disk) UnmarshalYAML(value *yamlv3.Node) error {
var v interface{}
if err := value.Decode(&v); err != nil {
Expand All @@ -33,23 +47,43 @@ func (d *Disk) UnmarshalYAML(value *yamlv3.Node) error {
return nil
}

func (d *Image) UnmarshalYAML(value *yamlv3.Node) error {
var v interface{}
if err := value.Decode(&v); err != nil {
return err
}
if s, ok := v.(string); ok {
*d = Image{Name: s}
}
return nil
}

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 := yamlv3.Unmarshal(data, v); 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
9 changes: 9 additions & 0 deletions pkg/limayaml/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ func Validate(y *LimaYAML, warn bool) error {
return errors.New("field `images` must be set")
}
for i, f := range y.Images {
if f.Name != "" {
if ReadImage == nil {
return fmt.Errorf("limayaml.ReadImage is not set")
}
if _, err := ReadImage(f.Name); err != nil {
return err
}
continue
}
if err := validateFileObject(f.File, fmt.Sprintf("images[%d]", i)); err != nil {
return err
}
Expand Down
9 changes: 9 additions & 0 deletions pkg/limayaml/validate_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package limayaml

import (
"fmt"
"os"
"runtime"
"testing"
Expand All @@ -17,6 +18,13 @@ func TestValidateEmpty(t *testing.T) {

// Note: can't embed symbolic links, use "os"

func readImage(name string) ([]byte, error) {
if name != "default" {
return nil, fmt.Errorf("unexpected image: %s", name)
}
return os.ReadFile("image.yaml")
}

func TestValidateDefault(t *testing.T) {
if runtime.GOOS == "windows" {
// FIXME: `assertion failed: error is not nil: field `mounts[1].location` must be an absolute path, got "/tmp/lima"`
Expand All @@ -25,6 +33,7 @@ func TestValidateDefault(t *testing.T) {

bytes, err := os.ReadFile("default.yaml")
assert.NilError(t, err)
ReadImage = readImage
y, err := Load(bytes, "default.yaml")
assert.NilError(t, err)
err = Validate(y, true)
Expand Down

0 comments on commit 38522fa

Please sign in to comment.