diff --git a/pkg/limatmpl/embed_test.go b/pkg/limatmpl/embed_test.go index f905787249d..1d37dc144ad 100644 --- a/pkg/limatmpl/embed_test.go +++ b/pkg/limatmpl/embed_test.go @@ -374,6 +374,12 @@ provision: "base: [{url: base.yaml, digest: deafbad}]", "not yet implemented", }, + { + "Image URLs will be converted into a template", + "", + "base: https://example.com/lima-linux-riscv64.img", + "{arch: riscv64, images: [{location: https://example.com/lima-linux-riscv64.img, arch: riscv64}]}", + }, } func TestEmbed(t *testing.T) { diff --git a/pkg/limatmpl/locator.go b/pkg/limatmpl/locator.go index 343ee5d7a3d..5afb7cb64eb 100644 --- a/pkg/limatmpl/locator.go +++ b/pkg/limatmpl/locator.go @@ -12,11 +12,14 @@ import ( "os" "path" "path/filepath" + "regexp" + "runtime" "strings" "unicode" "github.com/containerd/containerd/identifiers" "github.com/lima-vm/lima/pkg/ioutilx" + "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/templatestore" "github.com/sirupsen/logrus" ) @@ -30,6 +33,10 @@ func Read(ctx context.Context, name, locator string) (*Template, error) { Locator: locator, } + if imageTemplate(tmpl, locator) { + return tmpl, nil + } + isTemplateURL, templateURL := SeemsTemplateURL(locator) switch { case isTemplateURL: @@ -121,6 +128,97 @@ func Read(ctx context.Context, name, locator string) (*Template, error) { return tmpl, nil } +// Locators with an image file format extension, optionally followed by a compression method. +// This regex is also used to remove the file format suffix from the instance name. +var imageURLRegex = regexp.MustCompile(`\.(img|qcow2|raw|iso)(\.(gz|xz|bz2|zstd))?$`) + +// Image architecture will be guessed based on the presence of arch keywords. +var archKeywords = map[string]limayaml.Arch{ + "aarch64": limayaml.AARCH64, + "amd64": limayaml.X8664, + "arm64": limayaml.AARCH64, + "armhf": limayaml.ARMV7L, + "armv7l": limayaml.ARMV7L, + "riscv64": limayaml.RISCV64, + "x86_64": limayaml.X8664, +} + +// These generic tags will be stripped from an image name before turning it into an instance name. +var genericTags = []string{ + "base", // Fedora, Rocky + "cloud", // Fedora, openSUSE + "cloudimg", // Ubuntu, Arch + "cloudinit", // Alpine + "daily", // Debian + "default", // Gentoo + "generic", // Fedora + "genericcloud", // CentOS, Debian, Rocky, Alma + "kvm", // Oracle + "latest", // Gentoo, CentOS, Rocky, Alma + "linux", // Arch + "minimal", // openSUSE + "openstack", // Gentoo + "server", // Ubuntu + "std", // Alpine-Lima + "stream", // CentOS + "uefi", // Alpine + "vm", // openSUSE +} + +// imageTemplate checks if the locator specifies an image URL. +// It will create a minimal template with the image URL and arch derived from the image name +// and also set the default instance name to the image name, but stripped of generic tags. +func imageTemplate(tmpl *Template, locator string) bool { + if !imageURLRegex.MatchString(locator) { + return false + } + + var imageArch limayaml.Arch + for keyword, arch := range archKeywords { + pattern := fmt.Sprintf(`\b%s\b`, keyword) + if regexp.MustCompile(pattern).MatchString(locator) { + imageArch = arch + break + } + } + if imageArch == "" { + imageArch = limayaml.NewArch(runtime.GOARCH) + logrus.Warnf("cannot determine image arch from URL %q; assuming %q", locator, imageArch) + } + template := `arch: %q +images: +- location: %q + arch: %q +` + tmpl.Bytes = []byte(fmt.Sprintf(template, imageArch, locator, imageArch)) + tmpl.Name = InstNameFromImageURL(locator, imageArch) + return true +} + +func InstNameFromImageURL(locator, imageArch string) string { + // We intentionally call both path.Base and filepath.Base in case we are running on Windows. + name := strings.ToLower(filepath.Base(path.Base(locator))) + // Remove file format and compression file types + name = imageURLRegex.ReplaceAllString(name, "") + // The Alpine "nocloud_" prefix does not fit the genericTags pattern + name = strings.TrimPrefix(name, "nocloud_") + for _, tag := range genericTags { + re := regexp.MustCompile(fmt.Sprintf(`[-_.]%s\b`, tag)) + name = re.ReplaceAllString(name, "") + } + // Remove imageArch as well if it is the native arch + if limayaml.IsNativeArch(imageArch) { + re := regexp.MustCompile(fmt.Sprintf(`[-_.]%s\b`, imageArch)) + name = re.ReplaceAllString(name, "") + } + // Remove timestamps from name: 8 digit date, optionally followed by + // a delimiter and one or more digits before a word boundary + name = regexp.MustCompile(`[-_.]20\d{6}([-_.]\d+)?\b`).ReplaceAllString(name, "") + // Normalize archlinux name + name = regexp.MustCompile(`^arch\b`).ReplaceAllString(name, "archlinux") + return name +} + func SeemsTemplateURL(arg string) (bool, *url.URL) { u, err := url.Parse(arg) if err != nil { diff --git a/pkg/limatmpl/locator_test.go b/pkg/limatmpl/locator_test.go new file mode 100644 index 00000000000..dcd23361dd4 --- /dev/null +++ b/pkg/limatmpl/locator_test.go @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package limatmpl_test + +import ( + "fmt" + "runtime" + "testing" + + "github.com/lima-vm/lima/pkg/limatmpl" + "github.com/lima-vm/lima/pkg/limayaml" + "gotest.tools/v3/assert" +) + +func TestInstNameFromImageURL(t *testing.T) { + t.Run("strips image format and compression method", func(t *testing.T) { + name := limatmpl.InstNameFromImageURL("linux.iso.bz2", "unknown") + assert.Equal(t, name, "linux") + }) + t.Run("removes generic tags", func(t *testing.T) { + name := limatmpl.InstNameFromImageURL("linux-linux_cloudimg.base-x86_64.raw", "unknown") + assert.Equal(t, name, "linux-x86_64") + }) + t.Run("removes Alpine `nocloud_` prefix", func(t *testing.T) { + name := limatmpl.InstNameFromImageURL("nocloud_linux-x86_64.raw", "unknown") + assert.Equal(t, name, "linux-x86_64") + }) + t.Run("removes date tag", func(t *testing.T) { + name := limatmpl.InstNameFromImageURL("linux-20250101.raw", "unknown") + assert.Equal(t, name, "linux") + }) + t.Run("removes date tag including time", func(t *testing.T) { + name := limatmpl.InstNameFromImageURL("linux-20250101-2000.raw", "unknown") + assert.Equal(t, name, "linux") + }) + t.Run("removes date tag including zero time", func(t *testing.T) { + name := limatmpl.InstNameFromImageURL("linux-20250101.0.raw", "unknown") + assert.Equal(t, name, "linux") + }) + t.Run("replace arch with archlinux", func(t *testing.T) { + name := limatmpl.InstNameFromImageURL("arch-aarch64.raw", "unknown") + assert.Equal(t, name, "archlinux-aarch64") + }) + t.Run("don't replace arch in the middle of the name", func(t *testing.T) { + name := limatmpl.InstNameFromImageURL("my-arch-aarch64.raw", "unknown") + assert.Equal(t, name, "my-arch-aarch64") + }) + t.Run("removes native arch", func(t *testing.T) { + arch := limayaml.NewArch(runtime.GOARCH) + image := fmt.Sprintf("linux_cloudimg.base-%s.qcow2.gz", arch) + name := limatmpl.InstNameFromImageURL(image, arch) + assert.Equal(t, name, "linux") + }) +}