Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/limatmpl/locator.go
2614 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package limatmpl
5
6
import (
7
"context"
8
"errors"
9
"fmt"
10
"io"
11
"net/http"
12
"net/url"
13
"os"
14
"os/exec"
15
"path"
16
"path/filepath"
17
"regexp"
18
"runtime"
19
"strings"
20
21
"github.com/sirupsen/logrus"
22
23
"github.com/lima-vm/lima/v2/pkg/identifiers"
24
"github.com/lima-vm/lima/v2/pkg/ioutilx"
25
"github.com/lima-vm/lima/v2/pkg/limatype"
26
"github.com/lima-vm/lima/v2/pkg/limayaml"
27
"github.com/lima-vm/lima/v2/pkg/plugins"
28
"github.com/lima-vm/lima/v2/pkg/templatestore"
29
)
30
31
const yBytesLimit = 4 * 1024 * 1024 // 4MiB
32
33
func Read(ctx context.Context, name, locator string) (*Template, error) {
34
tmpl := &Template{
35
Name: name,
36
Locator: locator,
37
}
38
39
locator, err := TransformCustomURL(ctx, locator)
40
if err != nil {
41
return nil, err
42
}
43
44
if imageTemplate(tmpl, locator) {
45
return tmpl, nil
46
}
47
48
isTemplateURL, templateName := SeemsTemplateURL(locator)
49
switch {
50
case isTemplateURL:
51
logrus.Debugf("interpreting argument %q as a template name %q", locator, templateName)
52
if tmpl.Name == "" {
53
// e.g., templateName = "deprecated/centos-7.yaml" , tmpl.Name = "centos-7"
54
tmpl.Name, err = InstNameFromYAMLPath(templateName)
55
if err != nil {
56
return nil, err
57
}
58
}
59
tmpl.Bytes, err = templatestore.Read(templateName)
60
if err != nil {
61
return nil, err
62
}
63
case SeemsHTTPURL(locator):
64
if tmpl.Name == "" {
65
tmpl.Name, err = InstNameFromURL(locator)
66
if err != nil {
67
return nil, err
68
}
69
}
70
logrus.Debugf("interpreting argument %q as a http url for instance %q", locator, tmpl.Name)
71
req, err := http.NewRequestWithContext(ctx, http.MethodGet, locator, http.NoBody)
72
if err != nil {
73
return nil, err
74
}
75
resp, err := http.DefaultClient.Do(req)
76
if err != nil {
77
return nil, err
78
}
79
defer resp.Body.Close()
80
tmpl.Bytes, err = ioutilx.ReadAtMaximum(resp.Body, yBytesLimit)
81
if err != nil {
82
return nil, err
83
}
84
case SeemsFileURL(locator):
85
if tmpl.Name == "" {
86
tmpl.Name, err = InstNameFromURL(locator)
87
if err != nil {
88
return nil, err
89
}
90
}
91
logrus.Debugf("interpreting argument %q as a file URL for instance %q", locator, tmpl.Name)
92
filePath := strings.TrimPrefix(locator, "file://")
93
if !filepath.IsAbs(filePath) {
94
return nil, fmt.Errorf("file URL %q is not an absolute path", locator)
95
}
96
r, err := os.Open(filePath)
97
if err != nil {
98
return nil, err
99
}
100
defer r.Close()
101
tmpl.Bytes, err = ioutilx.ReadAtMaximum(r, yBytesLimit)
102
if err != nil {
103
return nil, err
104
}
105
case locator == "-":
106
tmpl.Bytes, err = io.ReadAll(os.Stdin)
107
if err != nil {
108
return nil, fmt.Errorf("unexpected error reading stdin: %w", err)
109
}
110
default:
111
if tmpl.Name == "" {
112
tmpl.Name, err = InstNameFromYAMLPath(locator)
113
if err != nil {
114
return nil, err
115
}
116
}
117
logrus.Debugf("interpreting argument %q as a file path for instance %q", locator, tmpl.Name)
118
if locator, err = filepath.Abs(locator); err != nil {
119
return nil, err
120
}
121
tmpl.Locator = locator
122
r, err := os.Open(locator)
123
if err != nil {
124
return nil, err
125
}
126
defer r.Close()
127
tmpl.Bytes, err = ioutilx.ReadAtMaximum(r, yBytesLimit)
128
if err != nil {
129
return nil, err
130
}
131
}
132
// The only reason not to call tmpl.UseAbsLocators() here is that `limactl tmpl copy --verbatim …`
133
// should create an unmodified copy of the template.
134
return tmpl, nil
135
}
136
137
// Locators with an image file format extension, optionally followed by a compression method.
138
// This regex is also used to remove the file format suffix from the instance name.
139
var imageURLRegex = regexp.MustCompile(`\.(img|qcow2|raw|iso)(\.(gz|xz|bz2|zstd))?$`)
140
141
// Image architecture will be guessed based on the presence of arch keywords.
142
var archKeywords = map[string]limatype.Arch{
143
"aarch64": limatype.AARCH64,
144
"amd64": limatype.X8664,
145
"arm64": limatype.AARCH64,
146
"armhf": limatype.ARMV7L,
147
"armv7l": limatype.ARMV7L,
148
"ppc64el": limatype.PPC64LE,
149
"ppc64le": limatype.PPC64LE,
150
"riscv64": limatype.RISCV64,
151
"s390x": limatype.S390X,
152
"x86_64": limatype.X8664,
153
}
154
155
// These generic tags will be stripped from an image name before turning it into an instance name.
156
var genericTags = []string{
157
"base", // Fedora, Rocky
158
"cloud", // Fedora, openSUSE
159
"cloudimg", // Ubuntu, Arch
160
"cloudinit", // Alpine
161
"daily", // Debian
162
"default", // Gentoo
163
"generic", // Fedora
164
"genericcloud", // CentOS, Debian, Rocky, Alma
165
"kvm", // Oracle
166
"latest", // Gentoo, CentOS, Rocky, Alma
167
"linux", // Arch
168
"minimal", // openSUSE
169
"openstack", // Gentoo
170
"server", // Ubuntu
171
"std", // Alpine-Lima
172
"stream", // CentOS
173
"uefi", // Alpine
174
"vm", // openSUSE
175
}
176
177
// imageTemplate checks if the locator specifies an image URL.
178
// It will create a minimal template with the image URL and arch derived from the image name
179
// and also set the default instance name to the image name, but stripped of generic tags.
180
func imageTemplate(tmpl *Template, locator string) bool {
181
if !imageURLRegex.MatchString(locator) {
182
return false
183
}
184
185
var imageArch limatype.Arch
186
for keyword, arch := range archKeywords {
187
pattern := fmt.Sprintf(`\b%s\b`, keyword)
188
if regexp.MustCompile(pattern).MatchString(locator) {
189
imageArch = arch
190
break
191
}
192
}
193
if imageArch == "" {
194
imageArch = limatype.NewArch(runtime.GOARCH)
195
logrus.Warnf("cannot determine image arch from URL %q; assuming %q", locator, imageArch)
196
}
197
template := `arch: %q
198
images:
199
- location: %q
200
arch: %q
201
`
202
tmpl.Bytes = fmt.Appendf(nil, template, imageArch, locator, imageArch)
203
tmpl.Name = InstNameFromImageURL(locator, imageArch)
204
return true
205
}
206
207
func InstNameFromImageURL(locator, imageArch string) string {
208
// We intentionally call both path.Base and filepath.Base in case we are running on Windows.
209
name := strings.ToLower(filepath.Base(path.Base(locator)))
210
// Remove file format and compression file types.
211
name = imageURLRegex.ReplaceAllString(name, "")
212
// The Alpine "nocloud_" prefix does not fit the genericTags pattern.
213
name = strings.TrimPrefix(name, "nocloud_")
214
for _, tag := range genericTags {
215
re := regexp.MustCompile(fmt.Sprintf(`[-_.]%s\b`, tag))
216
name = re.ReplaceAllString(name, "")
217
}
218
// Remove imageArch as well if it is the native arch.
219
if limayaml.IsNativeArch(imageArch) {
220
re := regexp.MustCompile(fmt.Sprintf(`[-_.]%s\b`, imageArch))
221
name = re.ReplaceAllString(name, "")
222
}
223
// Remove timestamps from name: 8 digit date, optionally followed by
224
// a delimiter and one or more digits before a word boundary.
225
name = regexp.MustCompile(`[-_.]20\d{6}([-_.]\d+)?\b`).ReplaceAllString(name, "")
226
// Normalize archlinux name
227
name = regexp.MustCompile(`^arch\b`).ReplaceAllString(name, "archlinux")
228
// Remove redundant major version, e.g. "rocky-8-8.10" becomes "rocky-8.10".
229
// Unfortunately regexp doesn't support back references, so we have to
230
// check manually if both numbers are the same.
231
re := regexp.MustCompile(`-(\d+)-(\d+)\.`)
232
name = re.ReplaceAllStringFunc(name, func(match string) string {
233
submatch := re.FindStringSubmatch(match)
234
if submatch[1] == submatch[2] {
235
// Replace -X-X. with -X.
236
return "-" + submatch[1] + "."
237
}
238
return match
239
})
240
return name
241
}
242
243
// SeemsTemplateURL returns true if the arg is a URL using the template scheme.
244
// When it returns true, it also returns the template name.
245
func SeemsTemplateURL(arg string) (isTemplate bool, templateName string) {
246
u, err := url.Parse(arg)
247
if err != nil {
248
return false, ""
249
}
250
if u.Scheme == "template" {
251
if u.Opaque == "" {
252
return true, path.Join(u.Host, u.Path)
253
}
254
return true, u.Opaque
255
}
256
return false, ""
257
}
258
259
// SeemsHTTPURL returns true if the arg is a URL using the http or https scheme.
260
func SeemsHTTPURL(arg string) bool {
261
u, err := url.Parse(arg)
262
if err != nil {
263
return false
264
}
265
if u.Scheme != "http" && u.Scheme != "https" {
266
return false
267
}
268
return true
269
}
270
271
// SeemsFileURL returns true if the arg is a URL using the file scheme.
272
func SeemsFileURL(arg string) bool {
273
u, err := url.Parse(arg)
274
if err != nil {
275
return false
276
}
277
return u.Scheme == "file"
278
}
279
280
func InstNameFromURL(urlStr string) (string, error) {
281
u, err := url.Parse(urlStr)
282
if err != nil {
283
return "", err
284
}
285
return InstNameFromYAMLPath(path.Base(u.Path))
286
}
287
288
func InstNameFromYAMLPath(yamlPath string) (string, error) {
289
s := strings.ToLower(filepath.Base(yamlPath))
290
s = strings.TrimSuffix(strings.TrimSuffix(s, ".yml"), ".yaml")
291
// "." is allowed in instance names, but replaced to "-" for hostnames.
292
// e.g., yaml: "ubuntu-24.04.yaml" , instance name: "ubuntu-24.04", hostname: "lima-ubuntu-24-04"
293
if err := identifiers.Validate(s); err != nil {
294
return "", fmt.Errorf("filename %q is invalid: %w", yamlPath, err)
295
}
296
return s, nil
297
}
298
299
func transformCustomURL(ctx context.Context, locator string) (string, error) {
300
u, err := url.Parse(locator)
301
if err != nil || len(u.Scheme) <= 1 {
302
return locator, nil
303
}
304
305
if u.Scheme == "template" {
306
if u.Opaque != "" {
307
return locator, nil
308
}
309
// Fix malformed "template:" URLs.
310
newLocator := "template:" + path.Join(u.Host, u.Path)
311
logrus.Warnf("Template locator %q should be written %q since Lima v2.0", locator, newLocator)
312
return newLocator, nil
313
}
314
315
if u.Scheme == "github" {
316
return transformGitHubURL(ctx, u.Opaque)
317
}
318
319
plugin, err := plugins.Find("url-" + u.Scheme)
320
if err != nil {
321
return "", err
322
}
323
if plugin == nil {
324
return locator, nil
325
}
326
327
currentPath := os.Getenv("PATH")
328
defer os.Setenv("PATH", currentPath)
329
err = plugins.UpdatePath()
330
if err != nil {
331
return "", err
332
}
333
334
cmd := exec.CommandContext(ctx, plugin.Path, strings.TrimPrefix(u.String(), u.Scheme+":"))
335
cmd.Env = os.Environ()
336
337
stdout, err := cmd.Output()
338
if err != nil {
339
var exitErr *exec.ExitError
340
if errors.As(err, &exitErr) {
341
stderrMsg := string(exitErr.Stderr)
342
if stderrMsg != "" {
343
return "", fmt.Errorf("command %q failed: %s", cmd.String(), strings.TrimSpace(stderrMsg))
344
}
345
}
346
return "", fmt.Errorf("command %q failed: %w", cmd.String(), err)
347
}
348
return strings.TrimSpace(string(stdout)), nil
349
}
350
351
func TransformCustomURL(ctx context.Context, locator string) (string, error) {
352
seen := make(map[string]bool)
353
origLocator := locator
354
githubSchemeDetected := false
355
356
for !seen[locator] {
357
seen[locator] = true
358
if strings.HasPrefix(locator, "github:") {
359
githubSchemeDetected = true
360
}
361
newLocator, err := transformCustomURL(ctx, locator)
362
if err != nil {
363
return "", err
364
}
365
if newLocator == locator {
366
if githubSchemeDetected {
367
logrus.Warn("The github: scheme is still EXPERIMENTAL")
368
}
369
return newLocator, nil
370
}
371
logrus.Debugf("Locator %q replaced with %q", locator, newLocator)
372
locator = newLocator
373
}
374
return "", fmt.Errorf("custom locator %q has a redirect loop", origLocator)
375
}
376
377