Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/limayaml/validate.go
2601 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package limayaml
5
6
import (
7
"context"
8
"errors"
9
"fmt"
10
"net"
11
"os"
12
"path"
13
"path/filepath"
14
"regexp"
15
"runtime"
16
"slices"
17
"strconv"
18
"strings"
19
"unicode"
20
21
"github.com/docker/go-units"
22
"github.com/sirupsen/logrus"
23
24
"github.com/lima-vm/lima/v2/pkg/driverutil"
25
"github.com/lima-vm/lima/v2/pkg/identifiers"
26
"github.com/lima-vm/lima/v2/pkg/limatype"
27
"github.com/lima-vm/lima/v2/pkg/localpathutil"
28
"github.com/lima-vm/lima/v2/pkg/networks"
29
"github.com/lima-vm/lima/v2/pkg/osutil"
30
"github.com/lima-vm/lima/v2/pkg/version"
31
"github.com/lima-vm/lima/v2/pkg/version/versionutil"
32
)
33
34
func Validate(y *limatype.LimaYAML, warn bool) error {
35
var errs error
36
37
if len(y.Base) > 0 {
38
errs = errors.Join(errs, errors.New("field `base` must be empty for YAML validation"))
39
}
40
41
if y.MinimumLimaVersion != nil {
42
if _, err := versionutil.Parse(*y.MinimumLimaVersion); err != nil {
43
errs = errors.Join(errs, fmt.Errorf("field `minimumLimaVersion` must be a semvar value, got %q: %w", *y.MinimumLimaVersion, err))
44
}
45
// Unparsable version.Version (like commit hashes or "<unknown>") is treated as "latest/greatest"
46
// and will pass all version comparisons, allowing development builds to work.
47
if !versionutil.GreaterEqual(version.Version, *y.MinimumLimaVersion) {
48
errs = errors.Join(errs, fmt.Errorf("template requires Lima version %q; this is only %q", *y.MinimumLimaVersion, version.Version))
49
}
50
}
51
52
switch *y.OS {
53
case limatype.LINUX:
54
default:
55
errs = errors.Join(errs, fmt.Errorf("field `os` must be %q; got %q", limatype.LINUX, *y.OS))
56
}
57
if !slices.Contains(limatype.ArchTypes, *y.Arch) {
58
errs = errors.Join(errs, fmt.Errorf("field `arch` must be one of %v; got %q", limatype.ArchTypes, *y.Arch))
59
}
60
61
if len(y.Images) == 0 {
62
errs = errors.Join(errs, errors.New("field `images` must be set"))
63
}
64
for i, f := range y.Images {
65
err := validateFileObject(f.File, fmt.Sprintf("images[%d]", i))
66
if err != nil {
67
errs = errors.Join(errs, err)
68
}
69
if f.Kernel != nil {
70
err := validateFileObject(f.Kernel.File, fmt.Sprintf("images[%d].kernel", i))
71
if err != nil {
72
errs = errors.Join(errs, err)
73
}
74
if f.Kernel.Arch != f.Arch {
75
errs = errors.Join(errs, fmt.Errorf("images[%d].kernel has unexpected architecture %q, must be %q", i, f.Kernel.Arch, f.Arch))
76
}
77
}
78
if f.Initrd != nil {
79
err := validateFileObject(*f.Initrd, fmt.Sprintf("images[%d].initrd", i))
80
if err != nil {
81
errs = errors.Join(errs, err)
82
}
83
if f.Initrd.Arch != f.Arch {
84
errs = errors.Join(errs, fmt.Errorf("images[%d].initrd has unexpected architecture %q, must be %q", i, f.Initrd.Arch, f.Arch))
85
}
86
}
87
}
88
89
if *y.CPUs == 0 {
90
errs = errors.Join(errs, errors.New("field `cpus` must be set"))
91
}
92
93
if _, err := units.RAMInBytes(*y.Memory); err != nil {
94
errs = errors.Join(errs, fmt.Errorf("field `memory` has an invalid value: %w", err))
95
}
96
97
if _, err := units.RAMInBytes(*y.Disk); err != nil {
98
errs = errors.Join(errs, fmt.Errorf("field `disk` has an invalid value: %w", err))
99
}
100
101
for i, disk := range y.AdditionalDisks {
102
if err := identifiers.Validate(disk.Name); err != nil {
103
errs = errors.Join(errs, fmt.Errorf("field `additionalDisks[%d].name is invalid`: %w", i, err))
104
}
105
}
106
107
for i, f := range y.Mounts {
108
if !filepath.IsAbs(f.Location) && !strings.HasPrefix(f.Location, "~") {
109
errs = errors.Join(errs, fmt.Errorf("field `mounts[%d].location` must be an absolute path, got %q",
110
i, f.Location))
111
}
112
// f.Location has already been expanded in FillDefaults(), but that function cannot return errors.
113
loc, err := localpathutil.Expand(f.Location)
114
if err != nil {
115
errs = errors.Join(errs, fmt.Errorf("field `mounts[%d].location` refers to an unexpandable path: %q: %w", i, f.Location, err))
116
}
117
st, err := os.Stat(loc)
118
if err != nil {
119
if !errors.Is(err, os.ErrNotExist) {
120
errs = errors.Join(errs, fmt.Errorf("field `mounts[%d].location` refers to an inaccessible path: %q: %w", i, f.Location, err))
121
}
122
if warn {
123
logrus.Warnf("field `mounts[%d].location` refers to a non-existent directory: %q:", i, f.Location)
124
}
125
} else if !st.IsDir() {
126
errs = errors.Join(errs, fmt.Errorf("field `mounts[%d].location` refers to a non-directory path: %q: %w", i, f.Location, err))
127
}
128
129
switch *f.MountPoint {
130
case "/", "/bin", "/dev", "/etc", "/home", "/opt", "/sbin", "/tmp", "/usr", "/var":
131
errs = errors.Join(errs, fmt.Errorf("field `mounts[%d].mountPoint` must not be a system path such as /etc or /usr", i))
132
// home directory defined in "cidata.iso:/user-data"
133
case *y.User.Home:
134
errs = errors.Join(errs, fmt.Errorf("field `mounts[%d].mountPoint` is the reserved internal home directory %q", i, *y.User.Home))
135
}
136
// There is no tilde-expansion for guest filenames
137
if strings.HasPrefix(*f.MountPoint, "~") {
138
errs = errors.Join(errs, fmt.Errorf("field `mounts[%d].mountPoint` must not start with \"~\"", i))
139
}
140
141
if _, err := units.RAMInBytes(*f.NineP.Msize); err != nil {
142
errs = errors.Join(errs, fmt.Errorf("field `msize` has an invalid value: %w", err))
143
}
144
}
145
146
if *y.SSH.LocalPort != 0 {
147
if err := validatePort("ssh.localPort", *y.SSH.LocalPort); err != nil {
148
errs = errors.Join(errs, err)
149
}
150
}
151
152
if y.MountType != nil {
153
switch *y.MountType {
154
case limatype.REVSSHFS, limatype.NINEP, limatype.VIRTIOFS, limatype.WSLMount:
155
default:
156
errs = errors.Join(errs, fmt.Errorf("field `mountType` must be %q or %q or %q, or %q, got %q", limatype.REVSSHFS, limatype.NINEP, limatype.VIRTIOFS, limatype.WSLMount, *y.MountType))
157
}
158
159
if slices.Contains(y.MountTypesUnsupported, *y.MountType) {
160
errs = errors.Join(errs, fmt.Errorf("field `mountType` must not be one of %v (`mountTypesUnsupported`), got %q", y.MountTypesUnsupported, *y.MountType))
161
}
162
}
163
164
if warn && runtime.GOOS != "linux" {
165
for i, mount := range y.Mounts {
166
if mount.Virtiofs.QueueSize != nil {
167
logrus.Warnf("field mounts[%d].virtiofs.queueSize is only supported on Linux", i)
168
}
169
}
170
}
171
172
// y.Firmware.LegacyBIOS is ignored for aarch64, but not a fatal error.
173
174
for i, p := range y.Provision {
175
if p.File != nil {
176
if p.File.URL != "" {
177
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].file.url` must be empty during validation (script should already be embedded)", i))
178
}
179
if p.File.Digest != nil {
180
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].file.digest` support is not yet implemented", i))
181
}
182
}
183
switch p.Mode {
184
case limatype.ProvisionModeSystem, limatype.ProvisionModeUser, limatype.ProvisionModeBoot, limatype.ProvisionModeData, limatype.ProvisionModeDependency, limatype.ProvisionModeAnsible, limatype.ProvisionModeYQ:
185
default:
186
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].mode` must one of %q, %q, %q, %q, %q, %q, or %q",
187
i, limatype.ProvisionModeSystem, limatype.ProvisionModeUser, limatype.ProvisionModeBoot, limatype.ProvisionModeData, limatype.ProvisionModeDependency, limatype.ProvisionModeAnsible, limatype.ProvisionModeYQ))
188
}
189
if p.Mode != limatype.ProvisionModeDependency && p.SkipDefaultDependencyResolution != nil {
190
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].mode` cannot set skipDefaultDependencyResolution, only valid on scripts of type %q",
191
i, limatype.ProvisionModeDependency))
192
}
193
194
// This can lead to fatal Panic if p.Path is nil, better to return an error here
195
switch p.Mode {
196
case limatype.ProvisionModeData, limatype.ProvisionModeYQ:
197
if p.Path == nil {
198
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].path` must not be empty when mode is %q", i, p.Mode))
199
return errs
200
}
201
if !path.IsAbs(*p.Path) {
202
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].path` must be an absolute path", i))
203
}
204
if p.Mode == limatype.ProvisionModeData && p.Content == nil {
205
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].content` must not be empty when mode is %q", i, p.Mode))
206
}
207
if p.Mode == limatype.ProvisionModeYQ && p.Expression == nil {
208
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].expression` must not be empty when mode is %q", i, p.Mode))
209
}
210
// FillDefaults makes sure that p.Permissions is not nil
211
if _, err := strconv.ParseInt(*p.Permissions, 8, 64); err != nil {
212
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].permissions` must be an octal number: %w", i, err))
213
}
214
default:
215
if (p.Script == nil || *p.Script == "") && p.Mode != limatype.ProvisionModeAnsible {
216
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].script` must not be empty", i))
217
}
218
if p.Content != nil {
219
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].content` can only be set when mode is %q", i, limatype.ProvisionModeData))
220
}
221
if p.Overwrite != nil {
222
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].overwrite` can only be set when mode is %q", i, limatype.ProvisionModeData))
223
}
224
if p.Owner != nil {
225
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].owner` can only be set when mode is %q", i, limatype.ProvisionModeData))
226
}
227
if p.Path != nil {
228
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].path` can only be set when mode is %q, or %q", i, limatype.ProvisionModeData, limatype.ProvisionModeYQ))
229
}
230
if p.Permissions != nil {
231
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].permissions` can only be set when mode is %q, or %q", i, limatype.ProvisionModeData, limatype.ProvisionModeYQ))
232
}
233
if p.Format != nil {
234
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].format` can only be set when mode is %q", i, limatype.ProvisionModeYQ))
235
}
236
}
237
if p.Playbook != "" {
238
if p.Mode != limatype.ProvisionModeAnsible {
239
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].playbook can only be set when mode is %q", i, limatype.ProvisionModeAnsible))
240
}
241
if p.Script != nil && *p.Script != "" {
242
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].script must be empty if playbook is set", i))
243
}
244
playbook := p.Playbook
245
if _, err := os.Stat(playbook); err != nil {
246
errs = errors.Join(errs, fmt.Errorf("field `provision[%d].playbook` refers to an inaccessible path: %q: %w", i, playbook, err))
247
}
248
logrus.Warnf("provision mode %q is deprecated, use `ansible-playbook %q` instead", limatype.ProvisionModeAnsible, playbook)
249
}
250
if p.Script != nil {
251
if strings.Contains(*p.Script, "LIMA_CIDATA") {
252
logrus.Warn("provisioning scripts should not reference the LIMA_CIDATA variables")
253
}
254
}
255
}
256
needsContainerdArchives := (y.Containerd.User != nil && *y.Containerd.User) || (y.Containerd.System != nil && *y.Containerd.System)
257
if needsContainerdArchives {
258
if len(y.Containerd.Archives) == 0 {
259
errs = errors.Join(errs, errors.New("field `containerd.archives` must be provided"))
260
}
261
for i, f := range y.Containerd.Archives {
262
err := validateFileObject(f, fmt.Sprintf("containerd.archives[%d]", i))
263
if err != nil {
264
errs = errors.Join(errs, err)
265
}
266
}
267
}
268
for i, p := range y.Probes {
269
if p.File != nil {
270
if p.File.URL != "" {
271
errs = errors.Join(errs, fmt.Errorf("field `probe[%d].file.url` must be empty during validation (script should already be embedded)", i))
272
}
273
if p.File.Digest != nil {
274
errs = errors.Join(errs, fmt.Errorf("field `probe[%d].file.digest` support is not yet implemented", i))
275
}
276
}
277
if p.Script != nil && !strings.HasPrefix(*p.Script, "#!") {
278
errs = errors.Join(errs, fmt.Errorf("field `probe[%d].script` must start with a '#!' line", i))
279
}
280
switch p.Mode {
281
case limatype.ProbeModeReadiness:
282
default:
283
errs = errors.Join(errs, fmt.Errorf("field `probe[%d].mode` can only be %q", i, limatype.ProbeModeReadiness))
284
}
285
}
286
for i, rule := range y.PortForwards {
287
field := fmt.Sprintf("portForwards[%d]", i)
288
if *rule.GuestIPMustBeZero && !rule.GuestIP.Equal(net.IPv4zero) {
289
errs = errors.Join(errs, fmt.Errorf("field `%s.guestIPMustBeZero` can only be true when field `%s.guestIP` is 0.0.0.0", field, field))
290
}
291
if rule.GuestPort != 0 {
292
if rule.GuestSocket != "" {
293
errs = errors.Join(errs, fmt.Errorf("field `%s.guestPort` must be 0 when field `%s.guestSocket` is set", field, field))
294
}
295
if rule.GuestPort != rule.GuestPortRange[0] {
296
errs = errors.Join(errs, fmt.Errorf("field `%s.guestPort` must match field `%s.guestPortRange[0]`", field, field))
297
}
298
// redundant validation to make sure the error contains the correct field name
299
if err := validatePort(field+".guestPort", rule.GuestPort); err != nil {
300
errs = errors.Join(errs, err)
301
}
302
}
303
if rule.HostPort != 0 {
304
if rule.HostSocket != "" {
305
errs = errors.Join(errs, fmt.Errorf("field `%s.hostPort` must be 0 when field `%s.hostSocket` is set", field, field))
306
}
307
if rule.HostPort != rule.HostPortRange[0] {
308
errs = errors.Join(errs, fmt.Errorf("field `%s.hostPort` must match field `%s.hostPortRange[0]`", field, field))
309
}
310
// redundant validation to make sure the error contains the correct field name
311
if err := validatePort(field+".hostPort", rule.HostPort); err != nil {
312
errs = errors.Join(errs, err)
313
}
314
}
315
for j := range 2 {
316
if err := validatePort(fmt.Sprintf("%s.guestPortRange[%d]", field, j), rule.GuestPortRange[j]); err != nil {
317
errs = errors.Join(errs, err)
318
}
319
if rule.HostSocket == "" {
320
if err := validatePort(fmt.Sprintf("%s.hostPortRange[%d]", field, j), rule.HostPortRange[j]); err != nil {
321
errs = errors.Join(errs, err)
322
}
323
}
324
}
325
if rule.GuestPortRange[0] > rule.GuestPortRange[1] {
326
errs = errors.Join(errs, fmt.Errorf("field `%s.guestPortRange[1]` must be greater than or equal to field `%s.guestPortRange[0]`", field, field))
327
}
328
if rule.HostPortRange[0] > rule.HostPortRange[1] {
329
errs = errors.Join(errs, fmt.Errorf("field `%s.hostPortRange[1]` must be greater than or equal to field `%s.hostPortRange[0]`", field, field))
330
}
331
if rule.GuestSocket != "" {
332
if !path.IsAbs(rule.GuestSocket) {
333
errs = errors.Join(errs, fmt.Errorf("field `%s.guestSocket` must be an absolute path, but is %q", field, rule.GuestSocket))
334
}
335
if rule.HostSocket == "" && rule.HostPortRange[1]-rule.HostPortRange[0] > 0 {
336
errs = errors.Join(errs, fmt.Errorf("field `%s.guestSocket` can only be mapped to a single port or socket. not a range", field))
337
}
338
}
339
if rule.HostSocket != "" {
340
if !filepath.IsAbs(rule.HostSocket) {
341
// should be unreachable because FillDefault() will prepend the instance directory to relative names
342
errs = errors.Join(errs, fmt.Errorf("field `%s.hostSocket` must be an absolute path, but is %q", field, rule.HostSocket))
343
}
344
if rule.GuestSocket == "" && rule.GuestPortRange[1]-rule.GuestPortRange[0] > 0 {
345
errs = errors.Join(errs, fmt.Errorf("field `%s.hostSocket` can only be mapped from a single port or socket. not a range", field))
346
}
347
} else if rule.GuestPortRange[1]-rule.GuestPortRange[0] != rule.HostPortRange[1]-rule.HostPortRange[0] {
348
errs = errors.Join(errs, fmt.Errorf("field `%s.hostPortRange` must specify the same number of ports as field `%s.guestPortRange`", field, field))
349
}
350
351
if len(rule.HostSocket) >= osutil.UnixPathMax {
352
errs = errors.Join(errs, fmt.Errorf("field `%s.hostSocket` must be less than UNIX_PATH_MAX=%d characters, but is %d",
353
field, osutil.UnixPathMax, len(rule.HostSocket)))
354
}
355
switch rule.Proto {
356
case limatype.ProtoTCP, limatype.ProtoUDP, limatype.ProtoAny:
357
default:
358
errs = errors.Join(errs, fmt.Errorf("field `%s.proto` must be %q, %q, or %q", field, limatype.ProtoTCP, limatype.ProtoUDP, limatype.ProtoAny))
359
}
360
if rule.Reverse && rule.GuestSocket == "" {
361
errs = errors.Join(errs, fmt.Errorf("field `%s.reverse` must be %t", field, false))
362
}
363
if rule.Reverse && rule.HostSocket == "" {
364
errs = errors.Join(errs, fmt.Errorf("field `%s.reverse` must be %t", field, false))
365
}
366
// Not validating that the various GuestPortRanges and HostPortRanges are not overlapping. Rules will be
367
// processed sequentially and the first matching rule for a guest port determines forwarding behavior.
368
}
369
for i, rule := range y.CopyToHost {
370
field := fmt.Sprintf("CopyToHost[%d]", i)
371
if rule.GuestFile != "" {
372
if !path.IsAbs(rule.GuestFile) {
373
errs = errors.Join(errs, fmt.Errorf("field `%s.guest` must be an absolute path, but is %q", field, rule.GuestFile))
374
}
375
}
376
if rule.HostFile != "" {
377
if !filepath.IsAbs(rule.HostFile) {
378
errs = errors.Join(errs, fmt.Errorf("field `%s.host` must be an absolute path, but is %q", field, rule.HostFile))
379
}
380
}
381
}
382
383
if y.HostResolver.Enabled != nil && *y.HostResolver.Enabled && len(y.DNS) > 0 {
384
errs = errors.Join(errs, errors.New("field `dns` must be empty when field `HostResolver.Enabled` is true"))
385
}
386
387
err := validateNetwork(y)
388
if err != nil {
389
errs = errors.Join(errs, err)
390
}
391
392
if warn {
393
warnExperimental(y)
394
}
395
396
// Validate Param settings
397
// Names must start with a letter, followed by any number of letters, digits, or underscores
398
validParamName := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`)
399
for param, value := range y.Param {
400
if !validParamName.MatchString(param) {
401
errs = errors.Join(errs, fmt.Errorf("param %q name does not match regex %q", param, validParamName.String()))
402
}
403
for _, r := range value {
404
if !unicode.IsPrint(r) && r != '\t' && r != ' ' {
405
errs = errors.Join(errs, fmt.Errorf("param %q value contains unprintable character %q", param, r))
406
}
407
}
408
}
409
if y.Plain != nil && *y.Plain {
410
const portRangeWarnThreshold = 10
411
for i, rule := range y.PortForwards {
412
guestRange := rule.GuestPortRange[1] - rule.GuestPortRange[0] + 1
413
hostRange := rule.HostPortRange[1] - rule.HostPortRange[0] + 1
414
if guestRange > portRangeWarnThreshold || hostRange > portRangeWarnThreshold {
415
logrus.Warnf("[plain mode] portForwards[%d] covers a range of more than %d ports (guest: %d, host: %d). All ports will be forwarded unconditionally, which may be inefficient.", i, portRangeWarnThreshold, guestRange, hostRange)
416
}
417
}
418
}
419
420
return errs
421
}
422
423
func validateFileObject(f limatype.File, fieldName string) error {
424
var errs error
425
if !strings.Contains(f.Location, "://") {
426
if _, err := localpathutil.Expand(f.Location); err != nil {
427
errs = errors.Join(errs, fmt.Errorf("field `%s.location` refers to an invalid local file path: %q: %w", fieldName, f.Location, err))
428
}
429
// f.Location does NOT need to be accessible, so we do NOT check os.Stat(f.Location)
430
}
431
if !slices.Contains(limatype.ArchTypes, f.Arch) {
432
errs = errors.Join(errs, fmt.Errorf("field `arch` must be one of %v; got %q", limatype.ArchTypes, f.Arch))
433
}
434
if f.Digest != "" {
435
if err := f.Digest.Validate(); err != nil {
436
errs = errors.Join(errs, fmt.Errorf("field `%s.digest` is invalid: %s: %w", fieldName, f.Digest.String(), err))
437
}
438
}
439
return errs
440
}
441
442
func validateNetwork(y *limatype.LimaYAML) error {
443
var errs error
444
interfaceName := make(map[string]int)
445
for i, nw := range y.Networks {
446
field := fmt.Sprintf("networks[%d]", i)
447
switch {
448
case nw.Lima != "":
449
nwCfg, err := networks.LoadConfig()
450
if err != nil {
451
return err
452
}
453
if nwCfg.Check(nw.Lima) != nil {
454
errs = errors.Join(errs, fmt.Errorf("field `%s.lima` references network %q which is not defined in networks.yaml", field, nw.Lima))
455
}
456
usernet, err := nwCfg.Usernet(nw.Lima)
457
if err != nil {
458
return err
459
}
460
if !usernet && runtime.GOOS != "darwin" {
461
errs = errors.Join(errs, fmt.Errorf("field `%s.lima` is only supported on macOS right now", field))
462
}
463
if nw.Socket != "" {
464
errs = errors.Join(errs, fmt.Errorf("field `%s.lima` and field `%s.socket` are mutually exclusive", field, field))
465
}
466
if nw.VZNAT != nil && *nw.VZNAT {
467
errs = errors.Join(errs, fmt.Errorf("field `%s.lima` and field `%s.vzNAT` are mutually exclusive", field, field))
468
}
469
case nw.Socket != "":
470
if nw.VZNAT != nil && *nw.VZNAT {
471
errs = errors.Join(errs, fmt.Errorf("field `%s.socket` and field `%s.vzNAT` are mutually exclusive", field, field))
472
}
473
if fi, err := os.Stat(nw.Socket); err != nil && !errors.Is(err, os.ErrNotExist) {
474
errs = errors.Join(errs, err)
475
} else if err == nil && fi.Mode()&os.ModeSocket == 0 {
476
errs = errors.Join(errs, fmt.Errorf("field `%s.socket` %q points to a non-socket file", field, nw.Socket))
477
}
478
case nw.VZNAT != nil && *nw.VZNAT:
479
if nw.Lima != "" {
480
errs = errors.Join(errs, fmt.Errorf("field `%s.vzNAT` and field `%s.lima` are mutually exclusive", field, field))
481
}
482
if nw.Socket != "" {
483
errs = errors.Join(errs, fmt.Errorf("field `%s.vzNAT` and field `%s.socket` are mutually exclusive", field, field))
484
}
485
default:
486
errs = errors.Join(errs, fmt.Errorf("field `%s.lima` or field `%s.socket must be set", field, field))
487
}
488
if nw.MACAddress != "" {
489
hw, err := net.ParseMAC(nw.MACAddress)
490
if err != nil {
491
errs = errors.Join(errs, fmt.Errorf("field `vmnet.mac` invalid: %w", err))
492
}
493
if len(hw) != 6 {
494
errs = errors.Join(errs, fmt.Errorf("field `%s.macAddress` must be a 48 bit (6 bytes) MAC address; actual length of %q is %d bytes", field, nw.MACAddress, len(hw)))
495
}
496
}
497
// FillDefault() will make sure that nw.Interface is not the empty string
498
if len(nw.Interface) >= 16 {
499
errs = errors.Join(errs, fmt.Errorf("field `%s.interface` must be less than 16 bytes, but is %d bytes: %q", field, len(nw.Interface), nw.Interface))
500
}
501
if strings.ContainsAny(nw.Interface, " \t\n/") {
502
errs = errors.Join(errs, fmt.Errorf("field `%s.interface` must not contain whitespace or slashes", field))
503
}
504
if nw.Interface == networks.SlirpNICName {
505
errs = errors.Join(errs, fmt.Errorf("field `%s.interface` must not be set to %q because it is reserved for slirp", field, networks.SlirpNICName))
506
}
507
if prev, ok := interfaceName[nw.Interface]; ok {
508
errs = errors.Join(errs, fmt.Errorf("field `%s.interface` value %q has already been used by field `networks[%d].interface`", field, nw.Interface, prev))
509
}
510
interfaceName[nw.Interface] = i
511
}
512
513
return errs
514
}
515
516
// validateParamIsUsed checks if the keys in the `param` field are used in any script, probe, copyToHost, or portForward.
517
// It should be called before the `y` parameter is passed to FillDefault() that execute template.
518
func validateParamIsUsed(y *limatype.LimaYAML) error {
519
for key := range y.Param {
520
re, err := regexp.Compile(`{{[^}]*\.Param\.` + key + `[^}]*}}|\bPARAM_` + key + `\b`)
521
if err != nil {
522
return fmt.Errorf("field to compile regexp for key %q: %w", key, err)
523
}
524
keyIsUsed := false
525
for _, p := range y.Provision {
526
for _, ptr := range []*string{p.Script, p.Content, p.Expression, p.Owner, p.Path, p.Permissions} {
527
if ptr != nil && re.MatchString(*ptr) {
528
keyIsUsed = true
529
break
530
}
531
}
532
if p.Playbook != "" {
533
playbook, err := os.ReadFile(p.Playbook)
534
if err != nil {
535
return err
536
}
537
if re.Match(playbook) {
538
keyIsUsed = true
539
break
540
}
541
}
542
}
543
for _, p := range y.Probes {
544
if p.Script != nil && re.MatchString(*p.Script) {
545
keyIsUsed = true
546
break
547
}
548
}
549
for _, p := range y.CopyToHost {
550
if re.MatchString(p.GuestFile) || re.MatchString(p.HostFile) {
551
keyIsUsed = true
552
break
553
}
554
}
555
for _, p := range y.PortForwards {
556
if re.MatchString(p.GuestSocket) || re.MatchString(p.HostSocket) {
557
keyIsUsed = true
558
break
559
}
560
}
561
for _, p := range y.Mounts {
562
if re.MatchString(p.Location) {
563
keyIsUsed = true
564
break
565
}
566
if p.MountPoint != nil && re.MatchString(*p.MountPoint) {
567
keyIsUsed = true
568
break
569
}
570
}
571
if !keyIsUsed {
572
return fmt.Errorf("field `param` key %q is not used in any provision, probe, copyToHost, or portForward", key)
573
}
574
}
575
return nil
576
}
577
578
func validatePort(field string, port int) error {
579
switch {
580
case port < 0:
581
return fmt.Errorf("field `%s` must be > 0", field)
582
case port == 0:
583
return fmt.Errorf("field `%s` must be set", field)
584
case port == 22:
585
return fmt.Errorf("field `%s` must not be 22", field)
586
case port > 65535:
587
return fmt.Errorf("field `%s` must be < 65536", field)
588
}
589
return nil
590
}
591
592
func warnExperimental(y *limatype.LimaYAML) {
593
if *y.MountType == limatype.VIRTIOFS && runtime.GOOS == "linux" {
594
logrus.Warn("`mountType: virtiofs` on Linux is experimental")
595
}
596
switch *y.Arch {
597
case limatype.RISCV64, limatype.ARMV7L, limatype.S390X, limatype.PPC64LE:
598
logrus.Warnf("`arch: %s ` is experimental", *y.Arch)
599
}
600
if y.Video.Display != nil && strings.Contains(*y.Video.Display, "vnc") {
601
logrus.Warn("`video.display: vnc` is experimental")
602
}
603
if y.Audio.Device != nil && *y.Audio.Device != "" {
604
logrus.Warn("`audio.device` is experimental")
605
}
606
if y.MountInotify != nil && *y.MountInotify {
607
logrus.Warn("`mountInotify` is experimental")
608
}
609
}
610
611
// ValidateAgainstLatestConfig validates the values between the latest YAML and the updated(New) YAML.
612
// This validates configuration rules that disallow certain changes, such as shrinking the disk.
613
func ValidateAgainstLatestConfig(ctx context.Context, yNew, yLatest []byte) error {
614
var n limatype.LimaYAML
615
var errs error
616
617
// Load the latest YAML and fill in defaults
618
l, err := LoadWithWarnings(ctx, yLatest, "")
619
if err != nil {
620
errs = errors.Join(errs, err)
621
}
622
if err := driverutil.ResolveVMType(ctx, l, ""); err != nil {
623
errs = errors.Join(errs, fmt.Errorf("failed to resolve vm for %q: %w", "", err))
624
}
625
if err := Unmarshal(yNew, &n, "Unmarshal new YAML bytes"); err != nil {
626
errs = errors.Join(errs, err)
627
}
628
629
// Handle editing the template without a disk value
630
if n.Disk == nil || l.Disk == nil {
631
return errs
632
}
633
634
// Disk value must be provided, as it is required when creating an instance.
635
nDisk, err := units.RAMInBytes(*n.Disk)
636
if err != nil {
637
errs = errors.Join(errs, err)
638
}
639
lDisk, err := units.RAMInBytes(*l.Disk)
640
if err != nil {
641
errs = errors.Join(errs, err)
642
}
643
644
// Reject shrinking disk
645
if nDisk < lDisk {
646
errs = errors.Join(errs, fmt.Errorf("field `disk`: shrinking the disk (%v --> %v) is not supported", *l.Disk, *n.Disk))
647
}
648
649
return errs
650
}
651
652