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 <[email protected]>
  • Loading branch information
afbjorklund committed Jun 12, 2024
1 parent ea92a26 commit 7019976
Show file tree
Hide file tree
Showing 14 changed files with 189 additions and 16 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,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
2 changes: 2 additions & 0 deletions cmd/limactl/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/lima-vm/lima/cmd/limactl/editflags"
"github.com/lima-vm/lima/pkg/editutil"
"github.com/lima-vm/lima/pkg/imagestore"
"github.com/lima-vm/lima/pkg/limayaml"
networks "github.com/lima-vm/lima/pkg/networks/reconcile"
"github.com/lima-vm/lima/pkg/start"
Expand Down Expand Up @@ -90,6 +91,7 @@ func editAction(cmd *cobra.Command, args []string) error {
logrus.Info("Aborting, no changes made to the instance")
return nil
}
limayaml.ReadImage = imagestore.Read
y, err := limayaml.Load(yBytes, filePath)
if err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions cmd/limactl/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/lima-vm/lima/cmd/limactl/editflags"
"github.com/lima-vm/lima/cmd/limactl/guessarg"
"github.com/lima-vm/lima/pkg/editutil"
"github.com/lima-vm/lima/pkg/imagestore"
"github.com/lima-vm/lima/pkg/ioutilx"
"github.com/lima-vm/lima/pkg/limayaml"
networks "github.com/lima-vm/lima/pkg/networks/reconcile"
Expand Down Expand Up @@ -323,6 +324,7 @@ func createInstance(ctx context.Context, st *creatorState, saveBrokenEditorBuffe
}
// limayaml.Load() needs to pass the store file path to limayaml.FillDefault() to calculate default MAC addresses
filePath := filepath.Join(instDir, filenames.LimaYAML)
limayaml.ReadImage = imagestore.Read
y, err := limayaml.Load(st.yBytes, filePath)
if err != nil {
return nil, err
Expand Down
3 changes: 3 additions & 0 deletions cmd/limactl/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"

"github.com/lima-vm/lima/cmd/limactl/guessarg"
"github.com/lima-vm/lima/pkg/imagestore"
"github.com/lima-vm/lima/pkg/limayaml"
"github.com/lima-vm/lima/pkg/store"
"github.com/spf13/cobra"

Expand All @@ -28,6 +30,7 @@ func validateAction(cmd *cobra.Command, args []string) error {
return err
}

limayaml.ReadImage = imagestore.Read
for _, f := range args {
y, err := store.LoadYAMLByFilePath(f)
if err != nil {
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-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"
- default

# CPUs
# 🟢 Builtin default: min(4, host CPU cores)
Expand Down
15 changes: 15 additions & 0 deletions images/ubuntu-24.04.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: "ubuntu:24.04"
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"
65 changes: 65 additions & 0 deletions pkg/imagestore/imagestore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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
}
name = strings.Replace(name, ":", "-", 1)
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
}
name := strings.TrimSuffix(strings.TrimPrefix(p, imagesDir+"/"), ".yaml")
x := Image{
// Name is like "ubuntu:24.04", "debian:12", ...
Name: strings.Replace(name, "-", ":", 1),
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 @@ -156,6 +156,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 @@ -195,6 +197,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
6 changes: 6 additions & 0 deletions pkg/limayaml/limayaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ type LimaYAML struct {
TimeZone *string `yaml:"timezone,omitempty" json:"timezone,omitempty"`
}

type ImageYAML struct {
Name string
Images []Image
}

type (
OS = string
Arch = string
Expand Down Expand Up @@ -93,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 @@ -69,6 +69,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 7019976

Please sign in to comment.