Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/limayaml/validate_test.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
"fmt"
8
"testing"
9
10
"gotest.tools/v3/assert"
11
12
"github.com/lima-vm/lima/v2/pkg/limatype"
13
"github.com/lima-vm/lima/v2/pkg/version"
14
)
15
16
func TestValidateEmpty(t *testing.T) {
17
y, err := Load(t.Context(), []byte{}, "empty.yaml")
18
assert.NilError(t, err)
19
err = Validate(y, false)
20
assert.Error(t, err, "field `images` must be set")
21
}
22
23
func TestValidateMinimumLimaVersion(t *testing.T) {
24
images := `images: [{"location": "/"}]`
25
26
tests := []struct {
27
name string
28
currentVersion string
29
minimumLimaVersion string
30
wantErr string
31
}{
32
{
33
name: "minimumLimaVersion less than current version",
34
currentVersion: "1.1.1-114-g5bf5e513",
35
minimumLimaVersion: "1.1.0",
36
wantErr: "",
37
},
38
{
39
name: "minimumLimaVersion greater than current version",
40
currentVersion: "1.1.1-114-g5bf5e513",
41
minimumLimaVersion: "1.1.2",
42
wantErr: `template requires Lima version "1.1.2"; this is only "1.1.1-114-g5bf5e513"`,
43
},
44
{
45
name: "invalid current version",
46
currentVersion: "<unknown>",
47
minimumLimaVersion: "0.8.0",
48
wantErr: "", // Unparsable versions are treated as "latest"
49
},
50
{
51
name: "invalid minimumLimaVersion",
52
currentVersion: "1.1.1-114-g5bf5e513",
53
minimumLimaVersion: "invalid",
54
wantErr: "field `minimumLimaVersion` must be a semvar value, got \"invalid\": invalid is not in dotted-tri format", // Only parse error, no comparison error
55
},
56
}
57
58
for _, tt := range tests {
59
t.Run(tt.name, func(t *testing.T) {
60
oldVersion := version.Version
61
version.Version = tt.currentVersion
62
t.Cleanup(func() { version.Version = oldVersion })
63
64
y, err := Load(t.Context(), []byte("minimumLimaVersion: "+tt.minimumLimaVersion+"\n"+images), "lima.yaml")
65
assert.NilError(t, err)
66
67
err = Validate(y, false)
68
if tt.wantErr == "" {
69
assert.NilError(t, err)
70
} else {
71
assert.Error(t, err, tt.wantErr)
72
}
73
})
74
}
75
}
76
77
func TestValidateDigest(t *testing.T) {
78
images := `images: [{"location": "https://cloud-images.ubuntu.com/releases/oracular/release-20250701/ubuntu-24.10-server-cloudimg-amd64.img",digest: "69f31d3208895e5f646e345fbc95190e5e311ecd1359a4d6ee2c0b6483ceca03"}]`
79
validProbe := `probes: [{"script": "#!foo"}]`
80
y, err := Load(t.Context(), []byte(validProbe+"\n"+images), "lima.yaml")
81
assert.NilError(t, err)
82
err = Validate(y, false)
83
assert.Error(t, err, "field `images[0].digest` is invalid: 69f31d3208895e5f646e345fbc95190e5e311ecd1359a4d6ee2c0b6483ceca03: invalid checksum digest format")
84
85
images2 := `images: [{"location": "https://cloud-images.ubuntu.com/releases/oracular/release-20250701/ubuntu-24.10-server-cloudimg-amd64.img",digest: "sha001:69f31d3208895e5f646e345fbc95190e5e311ecd1359a4d6ee2c0b6483ceca03"}]`
86
y2, err := Load(t.Context(), []byte(validProbe+"\n"+images2), "lima.yaml")
87
assert.NilError(t, err)
88
err = Validate(y2, false)
89
assert.Error(t, err, "field `images[0].digest` is invalid: sha001:69f31d3208895e5f646e345fbc95190e5e311ecd1359a4d6ee2c0b6483ceca03: unsupported digest algorithm")
90
}
91
92
func TestValidateProbes(t *testing.T) {
93
images := `images: [{"location": "/"}]`
94
validProbe := `probes: [{"script": "#!foo"}]`
95
y, err := Load(t.Context(), []byte(validProbe+"\n"+images), "lima.yaml")
96
assert.NilError(t, err)
97
98
err = Validate(y, false)
99
assert.NilError(t, err)
100
101
invalidProbe := `probes: [{"script": "foo"}]`
102
y, err = Load(t.Context(), []byte(invalidProbe+"\n"+images), "lima.yaml")
103
assert.NilError(t, err)
104
105
err = Validate(y, false)
106
assert.Error(t, err, "field `probe[0].script` must start with a '#!' line")
107
108
invalidProbe = `probes: [{file: {digest: decafbad}}]`
109
y, err = Load(t.Context(), []byte(invalidProbe+"\n"+images), "lima.yaml")
110
assert.NilError(t, err)
111
112
err = Validate(y, false)
113
assert.Error(t, err, "field `probe[0].file.digest` support is not yet implemented\n"+
114
"field `probe[0].script` must start with a '#!' line")
115
}
116
117
func TestValidateProvisionMode(t *testing.T) {
118
images := `images: [{location: /}]`
119
provisionBoot := `provision: [{mode: boot, script: "touch /tmp/param-$PARAM_BOOT"}]`
120
y, err := Load(t.Context(), []byte(provisionBoot+"\n"+images), "lima.yaml")
121
assert.NilError(t, err)
122
123
err = Validate(y, false)
124
assert.NilError(t, err)
125
126
provisionUser := `provision: [{mode: user, script: "touch /tmp/param-$PARAM_USER"}]`
127
y, err = Load(t.Context(), []byte(provisionUser+"\n"+images), "lima.yaml")
128
assert.NilError(t, err)
129
130
err = Validate(y, false)
131
assert.NilError(t, err)
132
133
provisionDependency := `provision: [{mode: ansible, script: "touch /tmp/param-$PARAM_DEPENDENCY"}]`
134
y, err = Load(t.Context(), []byte(provisionDependency+"\n"+images), "lima.yaml")
135
assert.NilError(t, err)
136
137
err = Validate(y, false)
138
assert.NilError(t, err)
139
140
provisionInvalid := `provision: [{mode: invalid}]`
141
y, err = Load(t.Context(), []byte(provisionInvalid+"\n"+images), "lima.yaml")
142
assert.NilError(t, err)
143
144
err = Validate(y, false)
145
assert.Error(t, err, "field `provision[0].mode` must one of \"system\", \"user\", \"boot\", \"data\", \"dependency\", \"ansible\", or \"yq\"\n"+
146
"field `provision[0].script` must not be empty")
147
}
148
149
func TestValidateProvisionData(t *testing.T) {
150
images := `images: [{location: /}]`
151
validData := `provision: [{mode: data, path: /tmp, content: hello}]`
152
y, err := Load(t.Context(), []byte(validData+"\n"+images), "lima.yaml")
153
assert.NilError(t, err)
154
155
err = Validate(y, false)
156
assert.NilError(t, err)
157
158
invalidData := `provision: [{mode: data, content: hello}]`
159
y, err = Load(t.Context(), []byte(invalidData+"\n"+images), "lima.yaml")
160
assert.NilError(t, err)
161
162
err = Validate(y, false)
163
assert.Error(t, err, "field `provision[0].path` must not be empty when mode is \"data\"")
164
165
invalidData = `provision: [{mode: data, path: /tmp, content: hello, permissions: 9}]`
166
y, err = Load(t.Context(), []byte(invalidData+"\n"+images), "lima.yaml")
167
assert.NilError(t, err)
168
169
err = Validate(y, false)
170
assert.ErrorContains(t, err, "provision[0].permissions` must be an octal number")
171
}
172
173
func TestValidateProvisionYQ(t *testing.T) {
174
images := `images: [{location: /}]`
175
param := `param: {"cdi": "true"}`
176
// Valid
177
validYQProvision := `provision: [{mode: yq, expression: ".features.cdi={{.Param.cdi}}", path: /tmp}]`
178
y, err := Load(t.Context(), []byte(param+"\n"+validYQProvision+"\n"+images), "lima.yaml")
179
assert.NilError(t, err)
180
err = Validate(y, false)
181
assert.NilError(t, err)
182
183
// Missing path
184
invalidYQProvision := `provision: [{mode: yq, expression: ".features.cdi={{.Param.cdi}}"}]`
185
y, err = Load(t.Context(), []byte(param+"\n"+invalidYQProvision+"\n"+images), "lima.yaml")
186
assert.NilError(t, err)
187
err = Validate(y, false)
188
assert.ErrorContains(t, err, "field `provision[0].path` must not be empty when mode is \"yq\"")
189
190
// non-absolute path
191
invalidYQProvision = `provision: [{mode: yq, expression: ".features.cdi={{.Param.cdi}}", path: tmp}]`
192
y, err = Load(t.Context(), []byte(param+"\n"+invalidYQProvision+"\n"+images), "lima.yaml")
193
assert.NilError(t, err)
194
err = Validate(y, false)
195
assert.ErrorContains(t, err, "field `provision[0].path` must be an absolute path")
196
197
// Missing expression
198
invalidYQProvision = `provision: [{mode: yq, path: "/{{.Param.cdi}}"}]`
199
y, err = Load(t.Context(), []byte(param+"\n"+invalidYQProvision+"\n"+images), "lima.yaml")
200
assert.NilError(t, err)
201
err = Validate(y, false)
202
assert.ErrorContains(t, err, "field `provision[0].expression` must not be empty when mode is \"yq\"")
203
204
// Invalid permissions
205
invalidYQProvision = `provision: [{mode: yq, expression: ".features.cdi={{.Param.cdi}}", path: /tmp, permissions: 9}]`
206
y, err = Load(t.Context(), []byte(param+"\n"+invalidYQProvision+"\n"+images), "lima.yaml")
207
assert.NilError(t, err)
208
err = Validate(y, false)
209
assert.ErrorContains(t, err, "provision[0].permissions` must be an octal number")
210
}
211
212
func TestValidateAdditionalDisks(t *testing.T) {
213
images := `images: [{"location": "/"}]`
214
215
validDisks := `
216
additionalDisks:
217
- name: "disk1"
218
- name: "disk2"
219
`
220
y, err := Load(t.Context(), []byte(validDisks+"\n"+images), "lima.yaml")
221
assert.NilError(t, err)
222
223
err = Validate(y, false)
224
assert.NilError(t, err)
225
226
invalidDisks := `
227
additionalDisks:
228
- name: ""
229
`
230
y, err = Load(t.Context(), []byte(invalidDisks+"\n"+images), "lima.yaml")
231
assert.NilError(t, err)
232
233
err = Validate(y, false)
234
assert.Error(t, err, "field `additionalDisks[0].name is invalid`: identifier must not be empty")
235
}
236
237
func TestValidateParamName(t *testing.T) {
238
images := `images: [{"location": "/"}]`
239
validProvision := `provision: [{"script": "echo $PARAM_name $PARAM_NAME $PARAM_Name_123"}]`
240
validParam := []string{
241
`param: {"name": "value"}`,
242
`param: {"NAME": "value"}`,
243
`param: {"Name_123": "value"}`,
244
}
245
for _, param := range validParam {
246
y, err := Load(t.Context(), []byte(param+"\n"+validProvision+"\n"+images), "lima.yaml")
247
assert.NilError(t, err)
248
249
err = Validate(y, false)
250
assert.NilError(t, err)
251
}
252
253
invalidProvision := `provision: [{"script": "echo $PARAM__Name $PARAM_3Name $PARAM_Last.Name"}]`
254
invalidParam := []string{
255
`param: {"_Name": "value"}`,
256
`param: {"3Name": "value"}`,
257
`param: {"Last.Name": "value"}`,
258
}
259
for _, param := range invalidParam {
260
y, err := Load(t.Context(), []byte(param+"\n"+invalidProvision+"\n"+images), "lima.yaml")
261
assert.NilError(t, err)
262
263
err = Validate(y, false)
264
assert.ErrorContains(t, err, "name does not match regex")
265
}
266
}
267
268
func TestValidateParamValue(t *testing.T) {
269
images := `images: [{"location": "/"}]`
270
provision := `provision: [{"script": "echo $PARAM_name"}]`
271
validParam := []string{
272
`param: {"name": ""}`,
273
`param: {"name": "foo bar"}`,
274
`param: {"name": "foo\tbar"}`,
275
`param: {"name": "Symbols ½ and emoji → 👀"}`,
276
}
277
for _, param := range validParam {
278
y, err := Load(t.Context(), []byte(param+"\n"+provision+"\n"+images), "lima.yaml")
279
assert.NilError(t, err)
280
281
err = Validate(y, false)
282
assert.NilError(t, err)
283
}
284
285
invalidParam := []string{
286
`param: {"name": "The end.\n"}`,
287
`param: {"name": "\r"}`,
288
}
289
for _, param := range invalidParam {
290
y, err := Load(t.Context(), []byte(param+"\n"+provision+"\n"+images), "lima.yaml")
291
assert.NilError(t, err)
292
293
err = Validate(y, false)
294
assert.ErrorContains(t, err, "value contains unprintable character")
295
}
296
}
297
298
func TestValidateParamIsUsed(t *testing.T) {
299
paramYaml := `param:
300
name: value`
301
_, err := Load(t.Context(), []byte(paramYaml), "paramIsNotUsed.yaml")
302
assert.Error(t, err, "field `param` key \"name\" is not used in any provision, probe, copyToHost, or portForward")
303
304
fieldsUsingParam := []string{
305
`mounts: [{"location": "/tmp/{{ .Param.name }}"}]`,
306
`mounts: [{"location": "/tmp", mountPoint: "/tmp/{{ .Param.name }}"}]`,
307
`provision: [{"script": "echo {{ .Param.name }}"}]`,
308
`provision: [{"script": "echo $PARAM_name"}]`,
309
`probes: [{"script": "echo {{ .Param.name }}"}]`,
310
`probes: [{"script": "echo $PARAM_name"}]`,
311
`copyToHost: [{"guest": "/tmp/{{ .Param.name }}", "host": "/tmp"}]`,
312
`copyToHost: [{"guest": "/tmp", "host": "/tmp/{{ .Param.name }}"}]`,
313
`portForwards: [{"guestSocket": "/tmp/{{ .Param.name }}", "hostSocket": "/tmp"}]`,
314
`portForwards: [{"guestSocket": "/tmp", "hostSocket": "/tmp/{{ .Param.name }}"}]`,
315
}
316
for _, fieldUsingParam := range fieldsUsingParam {
317
_, err = Load(t.Context(), []byte(fieldUsingParam+"\n"+paramYaml), "paramIsUsed.yaml")
318
//
319
assert.NilError(t, err)
320
}
321
322
// use "{{if eq .Param.rootful \"true\"}}…{{else}}…{{end}}" in provision, probe, copyToHost, and portForward
323
rootfulYaml := `param:
324
rootful: true`
325
fieldsUsingIfParamRootfulTrue := []string{
326
`mounts: [{"location": "/tmp/{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}", "mountPoint": "/tmp"}]`,
327
`mounts: [{"location": "/tmp", "mountPoint": "/tmp/{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}"}]`,
328
`provision: [{"script": "echo {{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}"}]`,
329
`probes: [{"script": "echo {{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}"}]`,
330
`copyToHost: [{"guest": "/tmp/{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}", "host": "/tmp"}]`,
331
`copyToHost: [{"guest": "/tmp", "host": "/tmp/{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}"}]`,
332
`portForwards: [{"guestSocket": "{{if eq .Param.rootful \"true\"}}/var/run{{else}}/run/user/{{.UID}}{{end}}/docker.sock", "hostSocket": "{{.Dir}}/sock/docker.sock"}]`,
333
`portForwards: [{"guestSocket": "/var/run/docker.sock", "hostSocket": "{{.Dir}}/sock/docker-{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}.sock"}]`,
334
}
335
for _, fieldUsingIfParamRootfulTrue := range fieldsUsingIfParamRootfulTrue {
336
_, err = Load(t.Context(), []byte(fieldUsingIfParamRootfulTrue+"\n"+rootfulYaml), "paramIsUsed.yaml")
337
//
338
assert.NilError(t, err)
339
}
340
341
// use rootFul instead of rootful
342
rootFulYaml := `param:
343
rootFul: true`
344
for _, fieldUsingIfParamRootfulTrue := range fieldsUsingIfParamRootfulTrue {
345
_, err = Load(t.Context(), []byte(fieldUsingIfParamRootfulTrue+"\n"+rootFulYaml), "paramIsUsed.yaml")
346
//
347
assert.Error(t, err, "field `param` key \"rootFul\" is not used in any provision, probe, copyToHost, or portForward")
348
}
349
}
350
351
func TestValidateMultipleErrors(t *testing.T) {
352
yamlWithMultipleErrors := `
353
os: windows
354
arch: unsupported_arch
355
portForwards:
356
- guestPort: 22
357
hostPort: 2222
358
- guestPort: 8080
359
hostPort: 65536
360
provision:
361
- mode: invalid_mode
362
script: echo test
363
- mode: data
364
content: test
365
`
366
367
y, err := Load(t.Context(), []byte(yamlWithMultipleErrors), "multiple-errors.yaml")
368
assert.NilError(t, err)
369
err = Validate(y, false)
370
t.Logf("Validation errors: %v", err)
371
372
assert.Error(t, err, "field `os` must be \"Linux\"; got \"windows\"\n"+
373
"field `arch` must be one of [x86_64 aarch64 armv7l ppc64le riscv64 s390x]; got \"unsupported_arch\"\n"+
374
"field `images` must be set\n"+
375
"field `provision[0].mode` must one of \"system\", \"user\", \"boot\", \"data\", \"dependency\", \"ansible\", or \"yq\"\n"+
376
"field `provision[1].path` must not be empty when mode is \"data\"")
377
}
378
379
func TestValidateAgainstLatestConfig(t *testing.T) {
380
tests := []struct {
381
name string
382
yNew string
383
yLatest string
384
wantErr string
385
}{
386
{
387
name: "Valid disk size unchanged",
388
yNew: `disk: 100GiB`,
389
yLatest: `disk: 100GiB`,
390
wantErr: fmt.Sprintf("failed to resolve vm for \"\": vmType %q is not a registered driver", limatype.DefaultDriver()),
391
},
392
{
393
name: "Valid disk size increased",
394
yNew: `disk: 200GiB`,
395
yLatest: `disk: 100GiB`,
396
wantErr: fmt.Sprintf("failed to resolve vm for \"\": vmType %q is not a registered driver", limatype.DefaultDriver()),
397
},
398
{
399
name: "No disk field in both YAMLs",
400
yNew: ``,
401
yLatest: ``,
402
wantErr: fmt.Sprintf("failed to resolve vm for \"\": vmType %q is not a registered driver", limatype.DefaultDriver()),
403
},
404
{
405
name: "No disk field in new YAMLs",
406
yNew: ``,
407
yLatest: `disk: 100GiB`,
408
wantErr: fmt.Sprintf("failed to resolve vm for \"\": vmType %q is not a registered driver", limatype.DefaultDriver()),
409
},
410
{
411
name: "No disk field in latest YAMLs",
412
yNew: `disk: 100GiB`,
413
yLatest: ``,
414
wantErr: fmt.Sprintf("failed to resolve vm for \"\": vmType %q is not a registered driver", limatype.DefaultDriver()),
415
},
416
{
417
name: "Disk size shrunk",
418
yNew: `disk: 50GiB`,
419
yLatest: `disk: 100GiB`,
420
wantErr: fmt.Sprintf("failed to resolve vm for \"\": vmType %q is not a registered driver\n", limatype.DefaultDriver()) +
421
"field `disk`: shrinking the disk (100GiB --> 50GiB) is not supported",
422
},
423
}
424
425
for _, tt := range tests {
426
t.Run(tt.name, func(t *testing.T) {
427
err := ValidateAgainstLatestConfig(t.Context(), []byte(tt.yNew), []byte(tt.yLatest))
428
assert.Error(t, err, tt.wantErr)
429
})
430
}
431
}
432
433