Skip to content

Commit 0cff556

Browse files
authored
Merge pull request #3338 from jandubois/image-templates
Allow an image URL instead of a template reference
2 parents c1994c5 + 4525227 commit 0cff556

File tree

3 files changed

+159
-0
lines changed

3 files changed

+159
-0
lines changed

pkg/limatmpl/embed_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,12 @@ provision:
386386
"base: [{url: base.yaml, digest: deafbad}]",
387387
"not yet implemented",
388388
},
389+
{
390+
"Image URLs will be converted into a template",
391+
"",
392+
"base: https://example.com/lima-linux-riscv64.img",
393+
"{arch: riscv64, images: [{location: https://example.com/lima-linux-riscv64.img, arch: riscv64}]}",
394+
},
389395
}
390396

391397
func TestEmbed(t *testing.T) {

pkg/limatmpl/locator.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import (
1212
"os"
1313
"path"
1414
"path/filepath"
15+
"regexp"
16+
"runtime"
1517
"strings"
1618
"unicode"
1719

1820
"github.com/containerd/containerd/identifiers"
1921
"github.com/lima-vm/lima/pkg/ioutilx"
22+
"github.com/lima-vm/lima/pkg/limayaml"
2023
"github.com/lima-vm/lima/pkg/templatestore"
2124
"github.com/sirupsen/logrus"
2225
)
@@ -30,6 +33,10 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
3033
Locator: locator,
3134
}
3235

36+
if imageTemplate(tmpl, locator) {
37+
return tmpl, nil
38+
}
39+
3340
isTemplateURL, templateURL := SeemsTemplateURL(locator)
3441
switch {
3542
case isTemplateURL:
@@ -121,6 +128,97 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
121128
return tmpl, nil
122129
}
123130

131+
// Locators with an image file format extension, optionally followed by a compression method.
132+
// This regex is also used to remove the file format suffix from the instance name.
133+
var imageURLRegex = regexp.MustCompile(`\.(img|qcow2|raw|iso)(\.(gz|xz|bz2|zstd))?$`)
134+
135+
// Image architecture will be guessed based on the presence of arch keywords.
136+
var archKeywords = map[string]limayaml.Arch{
137+
"aarch64": limayaml.AARCH64,
138+
"amd64": limayaml.X8664,
139+
"arm64": limayaml.AARCH64,
140+
"armhf": limayaml.ARMV7L,
141+
"armv7l": limayaml.ARMV7L,
142+
"riscv64": limayaml.RISCV64,
143+
"x86_64": limayaml.X8664,
144+
}
145+
146+
// These generic tags will be stripped from an image name before turning it into an instance name.
147+
var genericTags = []string{
148+
"base", // Fedora, Rocky
149+
"cloud", // Fedora, openSUSE
150+
"cloudimg", // Ubuntu, Arch
151+
"cloudinit", // Alpine
152+
"daily", // Debian
153+
"default", // Gentoo
154+
"generic", // Fedora
155+
"genericcloud", // CentOS, Debian, Rocky, Alma
156+
"kvm", // Oracle
157+
"latest", // Gentoo, CentOS, Rocky, Alma
158+
"linux", // Arch
159+
"minimal", // openSUSE
160+
"openstack", // Gentoo
161+
"server", // Ubuntu
162+
"std", // Alpine-Lima
163+
"stream", // CentOS
164+
"uefi", // Alpine
165+
"vm", // openSUSE
166+
}
167+
168+
// imageTemplate checks if the locator specifies an image URL.
169+
// It will create a minimal template with the image URL and arch derived from the image name
170+
// and also set the default instance name to the image name, but stripped of generic tags.
171+
func imageTemplate(tmpl *Template, locator string) bool {
172+
if !imageURLRegex.MatchString(locator) {
173+
return false
174+
}
175+
176+
var imageArch limayaml.Arch
177+
for keyword, arch := range archKeywords {
178+
pattern := fmt.Sprintf(`\b%s\b`, keyword)
179+
if regexp.MustCompile(pattern).MatchString(locator) {
180+
imageArch = arch
181+
break
182+
}
183+
}
184+
if imageArch == "" {
185+
imageArch = limayaml.NewArch(runtime.GOARCH)
186+
logrus.Warnf("cannot determine image arch from URL %q; assuming %q", locator, imageArch)
187+
}
188+
template := `arch: %q
189+
images:
190+
- location: %q
191+
arch: %q
192+
`
193+
tmpl.Bytes = []byte(fmt.Sprintf(template, imageArch, locator, imageArch))
194+
tmpl.Name = InstNameFromImageURL(locator, imageArch)
195+
return true
196+
}
197+
198+
func InstNameFromImageURL(locator, imageArch string) string {
199+
// We intentionally call both path.Base and filepath.Base in case we are running on Windows.
200+
name := strings.ToLower(filepath.Base(path.Base(locator)))
201+
// Remove file format and compression file types
202+
name = imageURLRegex.ReplaceAllString(name, "")
203+
// The Alpine "nocloud_" prefix does not fit the genericTags pattern
204+
name = strings.TrimPrefix(name, "nocloud_")
205+
for _, tag := range genericTags {
206+
re := regexp.MustCompile(fmt.Sprintf(`[-_.]%s\b`, tag))
207+
name = re.ReplaceAllString(name, "")
208+
}
209+
// Remove imageArch as well if it is the native arch
210+
if limayaml.IsNativeArch(imageArch) {
211+
re := regexp.MustCompile(fmt.Sprintf(`[-_.]%s\b`, imageArch))
212+
name = re.ReplaceAllString(name, "")
213+
}
214+
// Remove timestamps from name: 8 digit date, optionally followed by
215+
// a delimiter and one or more digits before a word boundary
216+
name = regexp.MustCompile(`[-_.]20\d{6}([-_.]\d+)?\b`).ReplaceAllString(name, "")
217+
// Normalize archlinux name
218+
name = regexp.MustCompile(`^arch\b`).ReplaceAllString(name, "archlinux")
219+
return name
220+
}
221+
124222
func SeemsTemplateURL(arg string) (bool, *url.URL) {
125223
u, err := url.Parse(arg)
126224
if err != nil {

pkg/limatmpl/locator_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package limatmpl_test
5+
6+
import (
7+
"fmt"
8+
"runtime"
9+
"testing"
10+
11+
"github.com/lima-vm/lima/pkg/limatmpl"
12+
"github.com/lima-vm/lima/pkg/limayaml"
13+
"gotest.tools/v3/assert"
14+
)
15+
16+
func TestInstNameFromImageURL(t *testing.T) {
17+
t.Run("strips image format and compression method", func(t *testing.T) {
18+
name := limatmpl.InstNameFromImageURL("linux.iso.bz2", "unknown")
19+
assert.Equal(t, name, "linux")
20+
})
21+
t.Run("removes generic tags", func(t *testing.T) {
22+
name := limatmpl.InstNameFromImageURL("linux-linux_cloudimg.base-x86_64.raw", "unknown")
23+
assert.Equal(t, name, "linux-x86_64")
24+
})
25+
t.Run("removes Alpine `nocloud_` prefix", func(t *testing.T) {
26+
name := limatmpl.InstNameFromImageURL("nocloud_linux-x86_64.raw", "unknown")
27+
assert.Equal(t, name, "linux-x86_64")
28+
})
29+
t.Run("removes date tag", func(t *testing.T) {
30+
name := limatmpl.InstNameFromImageURL("linux-20250101.raw", "unknown")
31+
assert.Equal(t, name, "linux")
32+
})
33+
t.Run("removes date tag including time", func(t *testing.T) {
34+
name := limatmpl.InstNameFromImageURL("linux-20250101-2000.raw", "unknown")
35+
assert.Equal(t, name, "linux")
36+
})
37+
t.Run("removes date tag including zero time", func(t *testing.T) {
38+
name := limatmpl.InstNameFromImageURL("linux-20250101.0.raw", "unknown")
39+
assert.Equal(t, name, "linux")
40+
})
41+
t.Run("replace arch with archlinux", func(t *testing.T) {
42+
name := limatmpl.InstNameFromImageURL("arch-aarch64.raw", "unknown")
43+
assert.Equal(t, name, "archlinux-aarch64")
44+
})
45+
t.Run("don't replace arch in the middle of the name", func(t *testing.T) {
46+
name := limatmpl.InstNameFromImageURL("my-arch-aarch64.raw", "unknown")
47+
assert.Equal(t, name, "my-arch-aarch64")
48+
})
49+
t.Run("removes native arch", func(t *testing.T) {
50+
arch := limayaml.NewArch(runtime.GOARCH)
51+
image := fmt.Sprintf("linux_cloudimg.base-%s.qcow2.gz", arch)
52+
name := limatmpl.InstNameFromImageURL(image, arch)
53+
assert.Equal(t, name, "linux")
54+
})
55+
}

0 commit comments

Comments
 (0)