From f8fdcf7032d0128280209f55978f6eb3163180c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sat, 8 Jun 2024 15:04:38 +0200 Subject: [PATCH] Allow loading images by name from imagestore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- Makefile | 1 + cmd/limactl/edit.go | 2 ++ cmd/limactl/start.go | 2 ++ cmd/limactl/validate.go | 3 ++ examples/default.yaml | 16 ++------- images/ubuntu-24.04.yaml | 15 ++++++++ pkg/imagestore/imagestore.go | 65 +++++++++++++++++++++++++++++++++++ pkg/infoutil/infoutil.go | 16 +++++++++ pkg/limayaml/defaults.go | 22 ++++++++++++ pkg/limayaml/image.yaml | 1 + pkg/limayaml/limayaml.go | 6 ++++ pkg/limayaml/load.go | 38 ++++++++++++++++++-- pkg/limayaml/validate.go | 9 +++++ pkg/limayaml/validate_test.go | 9 +++++ 14 files changed, 189 insertions(+), 16 deletions(-) create mode 100644 images/ubuntu-24.04.yaml create mode 100644 pkg/imagestore/imagestore.go create mode 120000 pkg/limayaml/image.yaml diff --git a/Makefile b/Makefile index 9b4506552a19..dc67de0bfe1e 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go index c6db7d50010d..b630f228abcf 100644 --- a/cmd/limactl/edit.go +++ b/cmd/limactl/edit.go @@ -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" @@ -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 diff --git a/cmd/limactl/start.go b/cmd/limactl/start.go index 89587af831b9..e7ba7a10f270 100644 --- a/cmd/limactl/start.go +++ b/cmd/limactl/start.go @@ -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" @@ -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 diff --git a/cmd/limactl/validate.go b/cmd/limactl/validate.go index 74e0e3a29274..5936aae7fc3f 100644 --- a/cmd/limactl/validate.go +++ b/cmd/limactl/validate.go @@ -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" @@ -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 { diff --git a/examples/default.yaml b/examples/default.yaml index def8e9dbf3cc..17e4007c527c 100644 --- a/examples/default.yaml +++ b/examples/default.yaml @@ -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) diff --git a/images/ubuntu-24.04.yaml b/images/ubuntu-24.04.yaml new file mode 100644 index 000000000000..3fc76371bcd4 --- /dev/null +++ b/images/ubuntu-24.04.yaml @@ -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" diff --git a/pkg/imagestore/imagestore.go b/pkg/imagestore/imagestore.go new file mode 100644 index 000000000000..2f03f954adb2 --- /dev/null +++ b/pkg/imagestore/imagestore.go @@ -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 +} diff --git a/pkg/infoutil/infoutil.go b/pkg/infoutil/infoutil.go index 19884e3b41ce..cecb441b9189 100644 --- a/pkg/infoutil/infoutil.go +++ b/pkg/infoutil/infoutil.go @@ -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" @@ -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 } @@ -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 diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index 99c4648188fa..079f09f97022 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -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. // @@ -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 == "" { diff --git a/pkg/limayaml/image.yaml b/pkg/limayaml/image.yaml new file mode 120000 index 000000000000..1330b6d87c91 --- /dev/null +++ b/pkg/limayaml/image.yaml @@ -0,0 +1 @@ +../../images/ubuntu-24.04.yaml \ No newline at end of file diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index f7bb6da5761e..a04862b352bc 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -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 @@ -93,6 +98,7 @@ type Kernel struct { } type Image struct { + Name string File `yaml:",inline"` Kernel *Kernel `yaml:"kernel,omitempty" json:"kernel,omitempty"` Initrd *File `yaml:"initrd,omitempty" json:"initrd,omitempty"` diff --git a/pkg/limayaml/load.go b/pkg/limayaml/load.go index f9c7e3c4ac5b..b6de3664958f 100644 --- a/pkg/limayaml/load.go +++ b/pkg/limayaml/load.go @@ -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 { @@ -33,8 +47,19 @@ 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 @@ -42,7 +67,7 @@ func unmarshalYAML(data []byte, v interface{}, comment string) error { 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 @@ -50,6 +75,15 @@ func unmarshalYAML(data []byte, v interface{}, comment string) error { 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. diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index ef4412f74d1d..11e0ae02e826 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -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 } diff --git a/pkg/limayaml/validate_test.go b/pkg/limayaml/validate_test.go index 751952cc23f8..5c5241bb1169 100644 --- a/pkg/limayaml/validate_test.go +++ b/pkg/limayaml/validate_test.go @@ -1,6 +1,7 @@ package limayaml import ( + "fmt" "os" "runtime" "testing" @@ -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"` @@ -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)