Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/limatmpl/embed.go
2604 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package limatmpl
5
6
import (
7
"bytes"
8
"context"
9
"encoding/base64"
10
"fmt"
11
"os"
12
"path/filepath"
13
"slices"
14
"strings"
15
"sync"
16
"unicode"
17
18
"github.com/coreos/go-semver/semver"
19
"github.com/sirupsen/logrus"
20
21
"github.com/lima-vm/lima/v2/pkg/limatype"
22
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
23
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
24
"github.com/lima-vm/lima/v2/pkg/limayaml"
25
"github.com/lima-vm/lima/v2/pkg/version/versionutil"
26
"github.com/lima-vm/lima/v2/pkg/yqutil"
27
)
28
29
// Embed will recursively resolve all "base" dependencies and update the
30
// template with the merged result. It also inlines all external provisioning
31
// and probe scripts.
32
func (tmpl *Template) Embed(ctx context.Context, embedAll, defaultBase bool) error {
33
if err := tmpl.UseAbsLocators(); err != nil {
34
return err
35
}
36
seen := make(map[string]bool)
37
err := tmpl.embedAllBases(ctx, embedAll, defaultBase, seen)
38
// additionalDisks, mounts, and networks may combine entries based on a shared key
39
// This must be done after **all** base templates have been merged, so that wildcard keys can match
40
// against all earlier list entries, and not just against the direct parent template.
41
if err == nil {
42
err = tmpl.combineListEntries()
43
}
44
return tmpl.ClearOnError(err)
45
}
46
47
func (tmpl *Template) embedAllBases(ctx context.Context, embedAll, defaultBase bool, seen map[string]bool) error {
48
logrus.Debugf("Embedding templates into %q", tmpl.Locator)
49
if defaultBase {
50
configDir, err := dirnames.LimaConfigDir()
51
if err != nil {
52
return err
53
}
54
defaultBaseFilename := filepath.Join(configDir, filenames.Base)
55
if _, err := os.Stat(defaultBaseFilename); err == nil {
56
// turn string into single element list
57
// empty concatenation works around bug https://github.com/mikefarah/yq/issues/2269
58
tmpl.expr.WriteString("| ($a.base | select(type == \"!!str\")) |= [\"\" + .]\n")
59
tmpl.expr.WriteString("| ($a.base | select(type == \"!!map\")) |= [[] + .]\n")
60
// prepend base template at the beginning of the list
61
tmpl.expr.WriteString(fmt.Sprintf("| $a.base = [%q, $a.base[]]\n", defaultBaseFilename))
62
if err := tmpl.evalExpr(); err != nil {
63
return err
64
}
65
}
66
}
67
for {
68
if err := tmpl.Unmarshal(); err != nil {
69
return err
70
}
71
if len(tmpl.Config.Base) == 0 {
72
break
73
}
74
baseLocator := tmpl.Config.Base[0]
75
if baseLocator.Digest != nil {
76
return fmt.Errorf("base %q in %q has specified a digest; digest support is not yet implemented", baseLocator.URL, tmpl.Locator)
77
}
78
isTemplate, _ := SeemsTemplateURL(baseLocator.URL)
79
if isTemplate && !embedAll {
80
// Once we skip a template: URL we can no longer embed any other base template
81
for i := 1; i < len(tmpl.Config.Base); i++ {
82
isTemplate, _ = SeemsTemplateURL(tmpl.Config.Base[i].URL)
83
if !isTemplate {
84
return fmt.Errorf("cannot embed template %q after not embedding %q", tmpl.Config.Base[i].URL, baseLocator.URL)
85
}
86
}
87
break
88
// TODO should we track embedding of template: URLs so we can warn if we embed a non-template: URL afterwards?
89
}
90
91
if seen[baseLocator.URL] {
92
return fmt.Errorf("base template loop detected: template %q already included", baseLocator.URL)
93
}
94
seen[baseLocator.URL] = true
95
96
// remove base[0] from template before merging
97
if err := tmpl.embedBase(ctx, baseLocator, embedAll, seen); err != nil {
98
return err
99
}
100
}
101
if err := tmpl.embedAllScripts(ctx, embedAll); err != nil {
102
return err
103
}
104
if len(tmpl.Bytes) > yBytesLimit {
105
return fmt.Errorf("template %q embedding exceeded the size limit (%d bytes)", tmpl.Locator, yBytesLimit)
106
}
107
return nil
108
}
109
110
func (tmpl *Template) embedBase(ctx context.Context, baseLocator limatype.LocatorWithDigest, embedAll bool, seen map[string]bool) error {
111
logrus.Debugf("Embedding base %q in template %q", baseLocator.URL, tmpl.Locator)
112
if err := tmpl.Unmarshal(); err != nil {
113
return err
114
}
115
base, err := Read(ctx, "", baseLocator.URL)
116
if err != nil {
117
return err
118
}
119
if err := base.UseAbsLocators(); err != nil {
120
return err
121
}
122
if err := base.embedAllBases(ctx, embedAll, false, seen); err != nil {
123
return err
124
}
125
if err := tmpl.merge(base); err != nil {
126
return err
127
}
128
if len(tmpl.Bytes) > yBytesLimit {
129
return fmt.Errorf("template %q embedding exceeded the size limit (%d bytes)", tmpl.Locator, yBytesLimit)
130
}
131
return nil
132
}
133
134
// evalExprImpl evaluates tmpl.expr against one or more documents.
135
// Called by evalExpr() and embedAllScripts() for single documents and merge() for 2 documents.
136
func (tmpl *Template) evalExprImpl(prefix string, b []byte) error {
137
var err error
138
expr := prefix + tmpl.expr.String() + "| $a"
139
tmpl.Bytes, err = yqutil.EvaluateExpression(expr, b)
140
// Make sure the YAML ends with just a single newline
141
tmpl.Bytes = append(bytes.TrimRight(tmpl.Bytes, "\n"), '\n')
142
tmpl.Config = nil
143
tmpl.expr.Reset()
144
return tmpl.ClearOnError(err)
145
}
146
147
// evalExpr evaluates tmpl.expr against the tmpl.Bytes document.
148
func (tmpl *Template) evalExpr() error {
149
var err error
150
if tmpl.expr.Len() > 0 {
151
// There is just a single document; $a and $b are the same
152
singleDocument := "select(document_index == 0) as $a | $a as $b\n"
153
err = tmpl.evalExprImpl(singleDocument, tmpl.Bytes)
154
}
155
return err
156
}
157
158
// merge merges the base template into tmpl.
159
func (tmpl *Template) merge(base *Template) error {
160
if err := tmpl.mergeBase(base); err != nil {
161
return tmpl.ClearOnError(err)
162
}
163
documents := fmt.Sprintf("%s\n---\n%s", string(tmpl.Bytes), string(base.Bytes))
164
return tmpl.evalExprImpl(mergeDocuments, []byte(documents))
165
}
166
167
// mergeBase generates a yq script to merge the template with a base.
168
// Most of the merging is done generically by the mergeDocuments script below.
169
// Only thing left is to compare minimum version numbers and keep the highest version.
170
func (tmpl *Template) mergeBase(base *Template) error {
171
if err := tmpl.Unmarshal(); err != nil {
172
return err
173
}
174
if err := base.Unmarshal(); err != nil {
175
return err
176
}
177
if tmpl.Config.MinimumLimaVersion != nil && base.Config.MinimumLimaVersion != nil {
178
if versionutil.GreaterThan(*base.Config.MinimumLimaVersion, *tmpl.Config.MinimumLimaVersion) {
179
const minimumLimaVersion = "minimumLimaVersion"
180
tmpl.copyField(minimumLimaVersion, minimumLimaVersion)
181
}
182
}
183
var tmplOpts limatype.QEMUOpts
184
if err := limayaml.Convert(tmpl.Config.VMOpts[limatype.QEMU], &tmplOpts, "vmOpts.qemu"); err != nil {
185
return err
186
}
187
var baseOpts limatype.QEMUOpts
188
if err := limayaml.Convert(base.Config.VMOpts[limatype.QEMU], &baseOpts, "vmOpts.qemu"); err != nil {
189
return err
190
}
191
if tmplOpts.MinimumVersion != nil && baseOpts.MinimumVersion != nil {
192
tmplVersion := *semver.New(*tmplOpts.MinimumVersion)
193
baseVersion := *semver.New(*baseOpts.MinimumVersion)
194
if tmplVersion.LessThan(baseVersion) {
195
const minimumQEMUVersion = "vmOpts.qemu.minimumVersion"
196
tmpl.copyField(minimumQEMUVersion, minimumQEMUVersion)
197
}
198
}
199
return nil
200
}
201
202
// mergeDocuments copies over settings from the base that don't yet exist
203
// in the template, and to append lists from the base to template lists.
204
// Both head and line comments are copied over as well.
205
//
206
// It also handles these special cases:
207
// * dns lists are not merged and only copied when the template doesn't have any dns entries at all.
208
// * probes and provision scripts are appended in reverse order.
209
// * mountTypesUnsupported have duplicate values removed.
210
// * base is removed from the template.
211
const mergeDocuments = `
212
select(document_index == 0) as $a
213
| select(document_index == 1) as $b
214
215
# $c will be mutilated to implement our own "merge only new fields" logic.
216
| $b as $c
217
218
# Delete the base that is being merged right now
219
| $a | select(.base | tag == "!!seq") | del(.base[0])
220
| $a | select(.base | (tag == "!!seq" and length == 0)) | del(.base)
221
| $a | select(.base | tag == "!!str") | del(.base)
222
223
# If $a.base is a list, then $b.base must be a list as well
224
# (note $b, not $c, because we merge lists from $b)
225
| $b | select((.base | tag == "!!str") and ($a.base | tag == "!!seq")) | .base = [ "" + .base ]
226
227
# Delete base DNS entries if the template list is not empty.
228
| $a | select(.dns) | del($b.dns, $c.dns)
229
230
# Mark all new list fields with a custom tag. This is needed to avoid appending
231
# newly copied lists to themselves again when we merge lists.
232
| $c | .. | select(tag == "!!seq") tag = "!!tag"
233
234
# Delete all nodes in $c that are in $a and not a map. This is necessary because
235
# the yq "*n" operator (merge only new fields) does not copy all comments across.
236
| $c | delpaths([$a | .. | select(tag != "!!map") | path])
237
238
# Merging with null returns null; use an empty map if $c has only comments
239
| $a * ($c // {}) as $a
240
241
# Find all elements that are existing lists. This will not match newly
242
# copied lists because they have a custom !!tag instead of !!seq.
243
# Append the elements from the same path in $b.
244
# Exception: base templates, provision scripts and probes are prepended instead.
245
| $a | (.. | select(tag == "!!seq" and (path[0] | test("^(base|provision|probes)$") | not))) |=
246
(. + (path[] as $p ireduce ($b; .[$p])))
247
| $a | (.. | select(tag == "!!seq" and (path[0] | test("^(base|provision|probes)$")))) |=
248
((path[] as $p ireduce ($b; .[$p])) + .)
249
250
# Copy head and line comments for existing lists that do not already have comments.
251
# New lists and existing maps already have their comments updated by the $a * $c merge.
252
| $a | (.. | select(tag == "!!seq" and (key | head_comment == "")) | key) head_comment |=
253
(((path[] as $p ireduce ($b; .[$p])) | key | head_comment) // "")
254
| $a | (.. | select(tag == "!!seq" and (key | line_comment == "")) | key) line_comment |=
255
(((path[] as $p ireduce ($b; .[$p])) | key | line_comment) // "")
256
257
# Make sure mountTypesUnsupported elements are unique.
258
| $a | (select(.mountTypesUnsupported) | .mountTypesUnsupported) |= unique
259
260
# Remove the custom tags again so they do not clutter up the YAML output.
261
| $a | .. | select(tag == "!!tag") tag = ""
262
`
263
264
// listFields returns dst and src fields like "list[idx].field".
265
func listFields(list string, dstIdx, srcIdx int, field string) (dst, src string) {
266
dst = fmt.Sprintf("%s[%d]", list, dstIdx)
267
src = fmt.Sprintf("%s[%d]", list, srcIdx)
268
if field != "" {
269
dst += "." + field
270
src += "." + field
271
}
272
return dst, src
273
}
274
275
// copyField copies value and comments from $b.src to $a.dst.
276
func (tmpl *Template) copyField(dst, src string) {
277
tmpl.expr.WriteString(fmt.Sprintf("| ($a.%s) = $b.%s\n", dst, src))
278
// The head_comment is on the key and not the value, so needs to be copied explicitly.
279
// Surprisingly the line_comment seems to be copied with the value already even though it is also on the key.
280
tmpl.expr.WriteString(fmt.Sprintf("| ($a.%s | key) head_comment = ($b.%s | key | head_comment)\n", dst, src))
281
}
282
283
// copyListEntryField copies $b.list[srcIdx].field to $a.list[dstIdx].field (including comments).
284
// Note: field must not be "" and must not be a list field itself either.
285
func (tmpl *Template) copyListEntryField(list string, dstIdx, srcIdx int, field string) {
286
tmpl.copyField(listFields(list, dstIdx, srcIdx, field))
287
}
288
289
type commentType string
290
291
const (
292
headComment commentType = "head"
293
lineComment commentType = "line"
294
)
295
296
// copyComment copies a non-empty head or line comment from $b.src to $a.dst, but only if $a.dst already exists.
297
func (tmpl *Template) copyComment(dst, src string, commentType commentType, isMapElement bool) {
298
onKey := ""
299
if isMapElement {
300
onKey = " | key" // For map elements the comments are on the keys and not the values.
301
}
302
// The expression is careful not to create a null $a.dst entry if $b.src has no comments and $a.dst didn't already exist.
303
// e.g.: `| $a | (select(.foo) | .foo | key | select(head_comment == "" and ($b.bar | key | head_comment != ""))) head_comment |= ($b.bar | key | head_comment)`
304
tmpl.expr.WriteString(fmt.Sprintf("| $a | (select(.%s) | .%s%s | select(%s_comment == \"\" and ($b.%s%s | %s_comment != \"\"))) %s_comment |= ($b.%s%s | %s_comment)\n",
305
dst, dst, onKey, commentType, src, onKey, commentType, commentType, src, onKey, commentType))
306
}
307
308
// copyComments copies all non-empty comments from $b.src to $a.dst.
309
func (tmpl *Template) copyComments(dst, src string, isMapElement bool) {
310
for _, commentType := range []commentType{headComment, lineComment} {
311
tmpl.copyComment(dst, src, commentType, isMapElement)
312
}
313
}
314
315
// copyListEntryComments copies all non-empty comments from $b.list[srcIdx].field to $a.list[dstIdx].field.
316
func (tmpl *Template) copyListEntryComments(list string, dstIdx, srcIdx int, field string) {
317
dst, src := listFields(list, dstIdx, srcIdx, field)
318
isMapElement := field != ""
319
tmpl.copyComments(dst, src, isMapElement)
320
}
321
322
func (tmpl *Template) deleteListEntry(list string, idx int) {
323
tmpl.expr.WriteString(fmt.Sprintf("| del($a.%s[%d], $b.%s[%d])\n", list, idx, list, idx))
324
}
325
326
// upgradeListEntryStringToMapField turns list[idx] from a string to a {field: list[idx]} map.
327
func (tmpl *Template) upgradeListEntryStringToMapField(list string, idx int, field string) {
328
// TODO the head_comment on the string becomes duplicated as a foot_comment on the new field; could be a yq bug?
329
tmpl.expr.WriteString(fmt.Sprintf("| ($a.%s[%d] | select(type == \"!!str\")) |= {\"%s\": .}\n", list, idx, field))
330
}
331
332
// combineListEntries combines entries based on a shared unique key.
333
// If two entries share the same key, then any missing fields in the earlier entry are
334
// filled in from the latter one. The latter one is then deleted.
335
//
336
// Notes:
337
// * The field order is not maintained when entries with a matching key are merged.
338
// * The unique keys (and mount locations) are assumed to not be subject to Go templating.
339
// * A wildcard key '*' matches all prior list entries.
340
func (tmpl *Template) combineListEntries() error {
341
if err := tmpl.Unmarshal(); err != nil {
342
return err
343
}
344
345
tmpl.combineAdditionalDisks()
346
tmpl.combineMounts()
347
tmpl.combineNetworks()
348
349
return tmpl.evalExpr()
350
}
351
352
// TODO: Maybe instead of hard-coding all the yaml names of LimaYAML struct fields we should
353
// TODO: retrieve them using reflection from the Go type tags to avoid possible typos.
354
355
// combineAdditionalDisks combines additionalDisks entries. The shared key is the disk name.
356
func (tmpl *Template) combineAdditionalDisks() {
357
const additionalDisks = "additionalDisks"
358
359
diskIdx := make(map[string]int, len(tmpl.Config.AdditionalDisks))
360
for src := 0; src < len(tmpl.Config.AdditionalDisks); {
361
disk := tmpl.Config.AdditionalDisks[src]
362
var from, to int
363
if disk.Name == "*" {
364
// copy to **all** previous entries
365
from = 0
366
to = src - 1
367
} else {
368
if i, ok := diskIdx[disk.Name]; ok {
369
// copy to previous disk with the same diskIdx
370
from = i
371
to = i
372
} else {
373
// record disk index and continue with the next entry
374
if disk.Name != "" {
375
diskIdx[disk.Name] = src
376
}
377
src++
378
continue
379
}
380
}
381
for dst := from; dst <= to; dst++ {
382
// upgrade additionalDisks[dst] from "disk" name string to {"name": "disk"} map so we can add fields
383
upgradeDiskToMap := sync.OnceFunc(func() {
384
tmpl.upgradeListEntryStringToMapField(additionalDisks, dst, "name")
385
})
386
387
dest := &tmpl.Config.AdditionalDisks[dst]
388
if dest.Format == nil && disk.Format != nil {
389
upgradeDiskToMap()
390
tmpl.copyListEntryField(additionalDisks, dst, src, "format")
391
dest.Format = disk.Format
392
}
393
// TODO: Does it make sense to merge "fsType" and "fsArgs" independently of each other?
394
if dest.FSType == nil && disk.FSType != nil {
395
upgradeDiskToMap()
396
tmpl.copyListEntryField(additionalDisks, dst, src, "fsType")
397
dest.FSType = disk.FSType
398
}
399
// "fsArgs" are inherited all-or-nothing; they are not appended
400
if len(dest.FSArgs) == 0 && len(disk.FSArgs) != 0 {
401
upgradeDiskToMap()
402
tmpl.copyListEntryField(additionalDisks, dst, src, "fsArgs")
403
dest.FSArgs = disk.FSArgs
404
}
405
// TODO: Is there a good reason not to copy comments from wildcard entries?
406
if disk.Name != "*" {
407
tmpl.copyListEntryComments(additionalDisks, dst, src, "")
408
}
409
}
410
tmpl.Config.AdditionalDisks = slices.Delete(tmpl.Config.AdditionalDisks, src, src+1)
411
tmpl.deleteListEntry(additionalDisks, src)
412
}
413
}
414
415
// combineMounts combines mounts entries. The shared key is the mount point.
416
func (tmpl *Template) combineMounts() {
417
const mounts = "mounts"
418
419
mountPointIdx := make(map[string]int, len(tmpl.Config.Mounts))
420
for src := 0; src < len(tmpl.Config.Mounts); {
421
mount := tmpl.Config.Mounts[src]
422
// mountPoint (an optional field) defaults to location (a required field)
423
mountPoint := mount.Location
424
if mount.MountPoint != nil {
425
mountPoint = *mount.MountPoint
426
}
427
var from, to int
428
if mountPoint == "*" {
429
from = 0
430
to = src - 1
431
} else {
432
if i, ok := mountPointIdx[mountPoint]; ok {
433
from = i
434
to = i
435
} else {
436
if mountPoint != "" {
437
mountPointIdx[mountPoint] = src
438
}
439
src++
440
continue
441
}
442
}
443
for dst := from; dst <= to; dst++ {
444
dest := &tmpl.Config.Mounts[dst]
445
// MountPoint
446
if dest.MountPoint == nil && mount.MountPoint != nil {
447
tmpl.copyListEntryField(mounts, dst, src, "mountPoint")
448
dest.MountPoint = mount.MountPoint
449
}
450
// Writable
451
if dest.Writable == nil && mount.Writable != nil {
452
tmpl.copyListEntryField(mounts, dst, src, "writable")
453
dest.Writable = mount.Writable
454
}
455
// SSHFS
456
if dest.SSHFS.Cache == nil && mount.SSHFS.Cache != nil {
457
tmpl.copyListEntryField(mounts, dst, src, "sshfs.cache")
458
dest.SSHFS.Cache = mount.SSHFS.Cache
459
}
460
if dest.SSHFS.FollowSymlinks == nil && mount.SSHFS.FollowSymlinks != nil {
461
tmpl.copyListEntryField(mounts, dst, src, "sshfs.followSymlinks")
462
dest.SSHFS.FollowSymlinks = mount.SSHFS.FollowSymlinks
463
}
464
if dest.SSHFS.SFTPDriver == nil && mount.SSHFS.SFTPDriver != nil {
465
tmpl.copyListEntryField(mounts, dst, src, "sshfs.sftpDriver")
466
dest.SSHFS.SFTPDriver = mount.SSHFS.SFTPDriver
467
}
468
// NineP
469
if dest.NineP.SecurityModel == nil && mount.NineP.SecurityModel != nil {
470
tmpl.copyListEntryField(mounts, dst, src, "9p.securityModel")
471
dest.NineP.SecurityModel = mount.NineP.SecurityModel
472
}
473
if dest.NineP.ProtocolVersion == nil && mount.NineP.ProtocolVersion != nil {
474
tmpl.copyListEntryField(mounts, dst, src, "9p.protocolVersion")
475
dest.NineP.ProtocolVersion = mount.NineP.ProtocolVersion
476
}
477
if dest.NineP.Msize == nil && mount.NineP.Msize != nil {
478
tmpl.copyListEntryField(mounts, dst, src, "9p.msize")
479
dest.NineP.Msize = mount.NineP.Msize
480
}
481
if dest.NineP.Cache == nil && mount.NineP.Cache != nil {
482
tmpl.copyListEntryField(mounts, dst, src, "9p.cache")
483
dest.NineP.Cache = mount.NineP.Cache
484
}
485
// Virtiofs
486
if dest.Virtiofs.QueueSize == nil && mount.Virtiofs.QueueSize != nil {
487
tmpl.copyListEntryField(mounts, dst, src, "virtiofs.queueSize")
488
dest.Virtiofs.QueueSize = mount.Virtiofs.QueueSize
489
}
490
if mountPoint != "*" {
491
tmpl.copyListEntryComments(mounts, dst, src, "")
492
tmpl.copyListEntryComments(mounts, dst, src, "sshfs")
493
tmpl.copyListEntryComments(mounts, dst, src, "9p")
494
tmpl.copyListEntryComments(mounts, dst, src, "virtiofs")
495
}
496
}
497
tmpl.Config.Mounts = slices.Delete(tmpl.Config.Mounts, src, src+1)
498
tmpl.deleteListEntry(mounts, src)
499
}
500
}
501
502
// combineNetworks combines networks entries. The shared key is the interface name.
503
func (tmpl *Template) combineNetworks() {
504
const networks = "networks"
505
506
interfaceIdx := make(map[string]int, len(tmpl.Config.Networks))
507
for src := 0; src < len(tmpl.Config.Networks); {
508
nw := tmpl.Config.Networks[src]
509
var from, to int
510
if nw.Interface == "*" {
511
from = 0
512
to = src - 1
513
} else {
514
if i, ok := interfaceIdx[nw.Interface]; ok {
515
from = i
516
to = i
517
} else {
518
if nw.Interface != "" {
519
interfaceIdx[nw.Interface] = src
520
}
521
src++
522
continue
523
}
524
}
525
for dst := from; dst <= to; dst++ {
526
dest := &tmpl.Config.Networks[dst]
527
// Lima and Socket are mutually exclusive. Only copy base values if both are still unset.
528
if dest.Lima == "" && dest.Socket == "" {
529
if nw.Lima != "" {
530
tmpl.copyListEntryField(networks, dst, src, "lima")
531
dest.Lima = nw.Lima
532
}
533
if nw.Socket != "" {
534
tmpl.copyListEntryField(networks, dst, src, "socket")
535
dest.Socket = nw.Socket
536
}
537
}
538
if dest.MACAddress == "" && nw.MACAddress != "" {
539
tmpl.copyListEntryField(networks, dst, src, "macAddress")
540
dest.MACAddress = nw.MACAddress
541
}
542
if dest.VZNAT == nil && nw.VZNAT != nil {
543
tmpl.copyListEntryField(networks, dst, src, "vzNAT")
544
dest.VZNAT = nw.VZNAT
545
}
546
if dest.Metric == nil && nw.Metric != nil {
547
tmpl.copyListEntryField(networks, dst, src, "metric")
548
dest.Metric = nw.Metric
549
}
550
if nw.Interface != "*" {
551
tmpl.copyListEntryComments(networks, dst, src, "")
552
}
553
}
554
tmpl.Config.Networks = slices.Delete(tmpl.Config.Networks, src, src+1)
555
tmpl.deleteListEntry(networks, src)
556
}
557
}
558
559
// yamlfmt will fail with a buffer overflow while trying to retain line breaks if the line
560
// is longer than 64K. We will encode all text files that have a line that comes close.
561
// maxLineLength is a constant; it is only a variable for the benefit of the unit tests.
562
var maxLineLength = 65000
563
564
// encodeScriptReason returns the reason why a script needs to be base64 encoded or the empty string if it doesn't.
565
func encodeScriptReason(script string) string {
566
start := 0
567
line := 1
568
for i, r := range script {
569
if !(unicode.IsPrint(r) || r == '\n') {
570
return fmt.Sprintf("unprintable character %q at offset %d", r, i)
571
}
572
// maxLineLength includes final newline
573
if i-start >= maxLineLength {
574
return fmt.Sprintf("line %d (offset %d) is longer than %d characters", line, start, maxLineLength)
575
}
576
if r == '\n' {
577
line++
578
start = i + 1
579
}
580
}
581
return ""
582
}
583
584
// Break base64 strings into shorter chunks. Technically we could use maxLineLength here,
585
// but shorter lines look better.
586
const base64ChunkLength = 76
587
588
// binaryString returns a base64 encoded version of the binary string, broken into chunks
589
// of at most base64ChunkLength characters per line.
590
func binaryString(s string) string {
591
encoded := base64.StdEncoding.EncodeToString([]byte(s))
592
if len(encoded) <= base64ChunkLength {
593
return encoded
594
}
595
596
// Estimate capacity: encoded length + number of newlines
597
lineCount := (len(encoded) + base64ChunkLength - 1) / base64ChunkLength
598
builder := strings.Builder{}
599
builder.Grow(len(encoded) + lineCount)
600
601
for i := 0; i < len(encoded); i += base64ChunkLength {
602
end := min(i+base64ChunkLength, len(encoded))
603
builder.WriteString(encoded[i:end])
604
builder.WriteByte('\n')
605
}
606
607
return builder.String()
608
}
609
610
// updateScript replaces a "file" property with the actual script and then renames the field to newName ("script" or "content").
611
func (tmpl *Template) updateScript(field string, idx int, newName, script, file string) {
612
tag := ""
613
if reason := encodeScriptReason(script); reason != "" {
614
logrus.Infof("File %q is being base64 encoded: %s", file, reason)
615
script = binaryString(script)
616
tag = "!!binary"
617
}
618
entry := fmt.Sprintf("$a.%s[%d].file", field, idx)
619
// Assign script to the "file" field and then rename it to "script" or "content".
620
tmpl.expr.WriteString(fmt.Sprintf("| (%s) = %q | (%s) tag = %q | (%s | key) = %q\n",
621
entry, script, entry, tag, entry, newName))
622
}
623
624
// embedAllScripts replaces all "provision" and "probes" file references with the actual script.
625
func (tmpl *Template) embedAllScripts(ctx context.Context, embedAll bool) error {
626
if err := tmpl.Unmarshal(); err != nil {
627
return err
628
}
629
for i, p := range tmpl.Config.Probes {
630
if p.File == nil {
631
continue
632
}
633
// Don't overwrite existing script. This should throw an error during validation.
634
if p.Script == nil || *p.Script != "" {
635
continue
636
}
637
isTemplate, _ := SeemsTemplateURL(p.File.URL)
638
if embedAll || !isTemplate {
639
scriptTmpl, err := Read(ctx, "", p.File.URL)
640
if err != nil {
641
return err
642
}
643
tmpl.updateScript("probes", i, "script", string(scriptTmpl.Bytes), p.File.URL)
644
}
645
}
646
for i, p := range tmpl.Config.Provision {
647
if p.File == nil {
648
continue
649
}
650
newName := "script"
651
switch p.Mode {
652
case limatype.ProvisionModeData:
653
newName = "content"
654
if p.Content != nil {
655
continue
656
}
657
case limatype.ProvisionModeYQ:
658
newName = "expression"
659
if p.Expression != nil {
660
continue
661
}
662
default:
663
if p.Script != nil && *p.Script != "" {
664
continue
665
}
666
}
667
isTemplate, _ := SeemsTemplateURL(p.File.URL)
668
if embedAll || !isTemplate {
669
scriptTmpl, err := Read(ctx, "", p.File.URL)
670
if err != nil {
671
return err
672
}
673
tmpl.updateScript("provision", i, newName, string(scriptTmpl.Bytes), p.File.URL)
674
}
675
}
676
return tmpl.evalExpr()
677
}
678
679