Skip to content
Merged
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
6 changes: 6 additions & 0 deletions pkg/limatmpl/embed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
98 changes: 98 additions & 0 deletions pkg/limatmpl/locator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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:
Expand Down Expand Up @@ -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
}
Copy link
Member

@AkihiroSuda AkihiroSuda Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MaybefileName.trimPrefix("nocloud_").splitWords[0].toLower() just suffices

e.g.,

  • nocloud_alpine-3.21.2-x86_64-uefi-cloudinit-r0.qcow2 -> alpine
  • ubuntu-24.10-server-cloudimg-arm64.img -> ubuntu

Arch-Linux-x86_64-cloudimg-20250201.304316.qcow2 may still needs an exceptional rule though, if we want the instance name to be archlinux, not arch

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of like the more descriptive names. You can always use --name ubuntu https//... if you want a really short name or predictable name.

So I would prefer to keep the current logic, unless you really dislike it.

Also don't want to add unit tests if you are going to make me remove it again. 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lima-vm/maintainers Thoughts?

Copy link
Member Author

@jandubois jandubois Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arch-Linux-x86_64-cloudimg-20250201.304316.qcow2 may still needs an exceptional rule though, if we want the instance name to be archlinux, not arch

I just noticed that I didn't add linux to the list of generic tags. I would want to strip that too, so the default instance name for this image would be arch-20250201.304316.

Copy link
Member Author

@jandubois jandubois Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've selected all image names from all our templates and filtered out just the amd64 ones:

find templates -name "*.yaml" -exec yq '.images[].location' {} \; | xargs basename | sed s/nocloud_// | sort -fu | grep -E 'amd|x86'

I've then manually transformed each of them into the instance name produced by the current algorithm:

almalinux-8
almalinux-8-8.10-20240819
almalinux-9
almalinux-9-9.5-20241120
alpine-3.21.2-r0
alpine-lima-3.20.3
arch
arch-20250201.304316
centos-10
centos-10-20250210.0
centos-9
centos-9-20250210.0
debian-11
debian-11-20241202-1949
debian-12
debian-12-20250210-2019
debian-sid
fedora-41-1.4
gentoo
ol8u10-b237
ol9u5-b253
opensuse-leap-15.6
opensuse-tumbleweed
rocky-8
rocky-8-8.10-20240528.0
rocky-9
rocky-9-9.5-20241118.0
ubuntu-20.04
ubuntu-22.04
ubuntu-24.04
ubuntu-24.10

I think the names look reasonable, and I would prefer them to just picking the first word from the string.

Copy link
Member Author

@jandubois jandubois Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a special rule to translate arch to archlinux and another rule to strip the datestamps, which leaves us with this list of instance names (duplicates pruned):

almalinux-8
almalinux-8-8.10
almalinux-9
almalinux-9-9.5
alpine-3.21.2-r0
alpine-lima-3.20.3
archlinux
centos-10
centos-9
debian-11
debian-12
debian-sid
fedora-41-1.4
gentoo
ol8u10-b237
ol9u5-b253
opensuse-leap-15.6
opensuse-tumbleweed
rocky-8
rocky-8-8.10
rocky-9
rocky-9-9.5
ubuntu-20.04
ubuntu-22.04
ubuntu-24.04
ubuntu-24.10


// 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 {
Expand Down
55 changes: 55 additions & 0 deletions pkg/limatmpl/locator_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
Loading