Skip to content

Commit 3faf293

Browse files
committed
Add optional support for specifying digests for template locators
Instead of `base: template.yaml` the user can write: ```yaml base: - url: template.yaml digest: decafbad ``` Same thing for `file` properties of provisoning scripts and probes. The digest values are currently being ignored; verification will happen in a later PR. Signed-off-by: Jan Dubois <[email protected]>
1 parent 8bee02c commit 3faf293

File tree

10 files changed

+76
-45
lines changed

10 files changed

+76
-45
lines changed

cmd/limactl/template.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ var templateCopyExample = ` Template locators are local files, file://, https:/
4747
# Copy default template to STDOUT
4848
limactl template copy template://default -
4949
50-
# Copy template from web location to local file
51-
limactl template copy https://example.com/lima.yaml mighty-machine.yaml
50+
# Copy template from web location to local file and embed all external references
51+
# (this does not embed template:// references)
52+
limactl template copy --embed https://example.com/lima.yaml mighty-machine.yaml
5253
`
5354

5455
func newTemplateCopyCommand() *cobra.Command {

pkg/limatmpl/abs.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func (tmpl *Template) useAbsLocators() error {
2626
return err
2727
}
2828
for i, baseLocator := range tmpl.Config.Base {
29-
locator, err := absPath(baseLocator, basePath)
29+
locator, err := absPath(baseLocator.URL, basePath)
3030
if err != nil {
3131
return err
3232
}
@@ -40,7 +40,7 @@ func (tmpl *Template) useAbsLocators() error {
4040
}
4141
for i, p := range tmpl.Config.Probes {
4242
if p.File != nil {
43-
locator, err := absPath(*p.File, basePath)
43+
locator, err := absPath(p.File.URL, basePath)
4444
if err != nil {
4545
return err
4646
}
@@ -49,7 +49,7 @@ func (tmpl *Template) useAbsLocators() error {
4949
}
5050
for i, p := range tmpl.Config.Provision {
5151
if p.File != nil {
52-
locator, err := absPath(*p.File, basePath)
52+
locator, err := absPath(p.File.URL, basePath)
5353
if err != nil {
5454
return err
5555
}
@@ -63,7 +63,7 @@ func (tmpl *Template) useAbsLocators() error {
6363
// On Windows filepath.Abs() only returns a "rooted" name, but does not add the volume name.
6464
// withVolume also normalizes all path separators to the platform native one.
6565
func withVolume(path string) (string, error) {
66-
if runtime.GOOS == "windows" && len(filepath.VolumeName(path)) == 0 {
66+
if runtime.GOOS == "windows" && filepath.VolumeName(path) == "" {
6767
root, err := filepath.Abs("/")
6868
if err != nil {
6969
return "", err

pkg/limatmpl/abs_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ func TestAbsPath(t *testing.T) {
225225
assert.ErrorContains(t, err, "volume")
226226
})
227227
}
228-
228+
229229
t.Run("", func(t *testing.T) {
230230
actual, err := absPath("foo", "template://")
231231
assert.NilError(t, err)

pkg/limatmpl/embed.go

+16-14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"sync"
1010

1111
"github.com/coreos/go-semver/semver"
12+
"github.com/lima-vm/lima/pkg/limayaml"
1213
"github.com/lima-vm/lima/pkg/store/dirnames"
1314
"github.com/lima-vm/lima/pkg/store/filenames"
1415
"github.com/lima-vm/lima/pkg/version/versionutil"
@@ -19,6 +20,7 @@ import (
1920
var warnBaseIsExperimental = sync.OnceFunc(func() {
2021
logrus.Warn("`base` is experimental")
2122
})
23+
2224
var warnFileIsExperimental = sync.OnceFunc(func() {
2325
logrus.Warn("`provision[*].file` and `probes[*].file` are experimental")
2426
})
@@ -69,23 +71,23 @@ func (tmpl *Template) embedAllBases(ctx context.Context, embedAll, defaultBase b
6971
break
7072
}
7173
baseLocator := tmpl.Config.Base[0]
72-
isTemplate, _ := SeemsTemplateURL(baseLocator)
74+
isTemplate, _ := SeemsTemplateURL(baseLocator.URL)
7375
if isTemplate && !embedAll {
7476
// Once we skip a template:// URL we can no longer embed any other base template
7577
for i := 1; i < len(tmpl.Config.Base); i++ {
76-
isTemplate, _ = SeemsTemplateURL(tmpl.Config.Base[i])
78+
isTemplate, _ = SeemsTemplateURL(tmpl.Config.Base[i].URL)
7779
if !isTemplate {
78-
return fmt.Errorf("cannot embed template %q after not embedding %q", tmpl.Config.Base[i], baseLocator)
80+
return fmt.Errorf("cannot embed template %q after not embedding %q", tmpl.Config.Base[i].URL, baseLocator.URL)
7981
}
8082
}
8183
break
8284
// TODO should we track embedding of template:// URLs so we can warn if we embed a non-template:// URL afterwards?
8385
}
8486

85-
if seen[baseLocator] {
86-
return fmt.Errorf("base template loop detected: template %q already included", baseLocator)
87+
if seen[baseLocator.URL] {
88+
return fmt.Errorf("base template loop detected: template %q already included", baseLocator.URL)
8789
}
88-
seen[baseLocator] = true
90+
seen[baseLocator.URL] = true
8991

9092
// remove base[0] from template before merging
9193
if err := tmpl.embedBase(ctx, baseLocator, embedAll, seen); err != nil {
@@ -101,13 +103,13 @@ func (tmpl *Template) embedAllBases(ctx context.Context, embedAll, defaultBase b
101103
return nil
102104
}
103105

104-
func (tmpl *Template) embedBase(ctx context.Context, baseLocator string, embedAll bool, seen map[string]bool) error {
106+
func (tmpl *Template) embedBase(ctx context.Context, baseLocator limayaml.LocatorWithDigest, embedAll bool, seen map[string]bool) error {
105107
warnBaseIsExperimental()
106-
logrus.Debugf("Embedding base %q in template %q", baseLocator, tmpl.Locator)
108+
logrus.Debugf("Embedding base %q in template %q", baseLocator.URL, tmpl.Locator)
107109
if err := tmpl.Unmarshal(); err != nil {
108110
return err
109111
}
110-
base, err := Read(ctx, "", baseLocator)
112+
base, err := Read(ctx, "", baseLocator.URL)
111113
if err != nil {
112114
return err
113115
}
@@ -308,7 +310,7 @@ func (tmpl *Template) deleteListEntry(list string, idx int) {
308310
tmpl.expr.WriteString(fmt.Sprintf("| del($a.%s[%d], $b.%s[%d])\n", list, idx, list, idx))
309311
}
310312

311-
// upgradeListEntryStringToMapField turns list[idx] from a string to a {field: list[idx]} map
313+
// upgradeListEntryStringToMapField turns list[idx] from a string to a {field: list[idx]} map.
312314
func (tmpl *Template) upgradeListEntryStringToMapField(list string, idx int, field string) {
313315
// TODO the head_comment on the string becomes duplicated as a foot_comment on the new field; could be a yq bug?
314316
tmpl.expr.WriteString(fmt.Sprintf("| ($a.%s[%d] | select(type == \"!!str\")) |= {\"%s\": .}\n", list, idx, field))
@@ -557,9 +559,9 @@ func (tmpl *Template) embedAllScripts(ctx context.Context, embedAll bool) error
557559
// Don't overwrite existing script. This should throw an error during validation.
558560
if p.File != nil && p.Script == "" {
559561
warnFileIsExperimental()
560-
isTemplate, _ := SeemsTemplateURL(*p.File)
562+
isTemplate, _ := SeemsTemplateURL(p.File.URL)
561563
if embedAll || !isTemplate {
562-
scriptTmpl, err := Read(ctx, "", *p.File)
564+
scriptTmpl, err := Read(ctx, "", p.File.URL)
563565
if err != nil {
564566
return err
565567
}
@@ -570,9 +572,9 @@ func (tmpl *Template) embedAllScripts(ctx context.Context, embedAll bool) error
570572
for i, p := range tmpl.Config.Provision {
571573
if p.File != nil && p.Script == "" {
572574
warnFileIsExperimental()
573-
isTemplate, _ := SeemsTemplateURL(*p.File)
575+
isTemplate, _ := SeemsTemplateURL(p.File.URL)
574576
if embedAll || !isTemplate {
575-
scriptTmpl, err := Read(ctx, "", *p.File)
577+
scriptTmpl, err := Read(ctx, "", p.File.URL)
576578
if err != nil {
577579
return err
578580
}

pkg/limatmpl/embed_test.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ package limatmpl_test
33
import (
44
"context"
55
"fmt"
6-
"github.com/sirupsen/logrus"
76
"os"
87
"reflect"
98
"strings"
109
"testing"
1110

1211
"github.com/lima-vm/lima/pkg/limatmpl"
1312
"github.com/lima-vm/lima/pkg/limayaml"
13+
"github.com/sirupsen/logrus"
1414
"gotest.tools/v3/assert"
1515
"gotest.tools/v3/assert/cmp"
1616
)
@@ -179,13 +179,16 @@ mounts:
179179
},
180180
{
181181
"template:// URLs are not embedded when embedAll is false",
182+
// also tests file.url format
182183
``,
183184
`
184185
base: template://default
185186
provision:
186-
- file: template://provision.sh
187+
- file:
188+
url: template://provision.sh
187189
probes:
188-
- file: template://probe.sh
190+
- file:
191+
url: template://probe.sh
189192
`,
190193
`
191194
base: template://default
@@ -228,7 +231,7 @@ base: baseX.yaml`,
228231
"Bases are embedded depth-first",
229232
`#`,
230233
`
231-
base: [base1.yaml, base2.yaml]
234+
base: [base1.yaml, {url: base2.yaml}] # also test file.url format
232235
additionalDisks: [disk0]
233236
---
234237
base: base3.yaml

pkg/limatmpl/locator.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
7272
return nil, err
7373
}
7474
}
75-
logrus.Debugf("interpreting argument %q as a file url for instance %q", locator, tmpl.Name)
75+
logrus.Debugf("interpreting argument %q as a file URL for instance %q", locator, tmpl.Name)
7676
filePath := strings.TrimPrefix(locator, "file://")
7777
if !filepath.IsAbs(filePath) {
7878
return nil, fmt.Errorf("file URL %q is not an absolute path", locator)

pkg/limayaml/limayaml.go

+16-11
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ type LimaYAML struct {
5151
User User `yaml:"user,omitempty" json:"user,omitempty"`
5252
}
5353

54-
type BaseTemplates []string
54+
type BaseTemplates []LocatorWithDigest
55+
56+
type LocatorWithDigest struct {
57+
URL string `yaml:"url" json:"url"`
58+
Digest *string `yaml:"digest,omitempty" json:"digest,omitempty" jsonschema:"nullable"` // TODO currently unused
59+
}
5560

5661
type (
5762
OS = string
@@ -216,11 +221,11 @@ const (
216221
)
217222

218223
type Provision struct {
219-
Mode ProvisionMode `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=system"`
220-
SkipDefaultDependencyResolution *bool `yaml:"skipDefaultDependencyResolution,omitempty" json:"skipDefaultDependencyResolution,omitempty"`
221-
Script string `yaml:"script" json:"script"`
222-
File *string `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
223-
Playbook string `yaml:"playbook,omitempty" json:"playbook,omitempty"`
224+
Mode ProvisionMode `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=system"`
225+
SkipDefaultDependencyResolution *bool `yaml:"skipDefaultDependencyResolution,omitempty" json:"skipDefaultDependencyResolution,omitempty"`
226+
Script string `yaml:"script" json:"script"`
227+
File *LocatorWithDigest `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
228+
Playbook string `yaml:"playbook,omitempty" json:"playbook,omitempty"`
224229
}
225230

226231
type Containerd struct {
@@ -236,11 +241,11 @@ const (
236241
)
237242

238243
type Probe struct {
239-
Mode ProbeMode `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=readiness"`
240-
Description string `yaml:"description,omitempty" json:"description,omitempty"`
241-
Script string `yaml:"script,omitempty" json:"script,omitempty"`
242-
File *string `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
243-
Hint string `yaml:"hint,omitempty" json:"hint,omitempty"`
244+
Mode ProbeMode `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=readiness"`
245+
Description string `yaml:"description,omitempty" json:"description,omitempty"`
246+
Script string `yaml:"script,omitempty" json:"script,omitempty"`
247+
File *LocatorWithDigest `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
248+
Hint string `yaml:"hint,omitempty" json:"hint,omitempty"`
244249
}
245250

246251
type Proto = string

pkg/limayaml/marshal.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,21 @@ func unmarshalDisk(dst *Disk, b []byte) error {
3535
return yaml.Unmarshal(b, dst)
3636
}
3737

38-
// unmarshalBaseTemplates unmarshalls `base` which is either a string or a list of strings.
38+
// unmarshalBaseTemplates unmarshalls `base` which is either a string or a list of Locators.
3939
func unmarshalBaseTemplates(dst *BaseTemplates, b []byte) error {
4040
var s string
4141
if err := yaml.Unmarshal(b, &s); err == nil {
42-
*dst = BaseTemplates{s}
42+
*dst = BaseTemplates{LocatorWithDigest{URL: s}}
43+
return nil
44+
}
45+
return yaml.UnmarshalWithOptions(b, dst, yaml.CustomUnmarshaler[LocatorWithDigest](unmarshalLocatorWithDigest))
46+
}
47+
48+
// unmarshalLocator unmarshalls a locator which is either a string or a Locator struct.
49+
func unmarshalLocatorWithDigest(dst *LocatorWithDigest, b []byte) error {
50+
var s string
51+
if err := yaml.Unmarshal(b, &s); err == nil {
52+
*dst = LocatorWithDigest{URL: s}
4353
return nil
4454
}
4555
return yaml.Unmarshal(b, dst)
@@ -49,6 +59,7 @@ func Unmarshal(data []byte, v any, comment string) error {
4959
opts := []yaml.DecodeOption{
5060
yaml.CustomUnmarshaler[BaseTemplates](unmarshalBaseTemplates),
5161
yaml.CustomUnmarshaler[Disk](unmarshalDisk),
62+
yaml.CustomUnmarshaler[LocatorWithDigest](unmarshalLocatorWithDigest),
5263
}
5364
if err := yaml.UnmarshalWithOptions(data, v, opts...); err != nil {
5465
return fmt.Errorf("failed to unmarshal YAML (%s): %w", comment, err)

pkg/limayaml/validate.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ func Validate(y *LimaYAML, warn bool) error {
206206
// y.Firmware.LegacyBIOS is ignored for aarch64, but not a fatal error.
207207

208208
for i, p := range y.Provision {
209-
if p.File != nil && *p.File != "" {
210-
return fmt.Errorf("field `provision[%d].file` must be empty during validation (script should already be embedded)", i)
209+
if p.File != nil && p.File.URL != "" {
210+
return fmt.Errorf("field `provision[%d].file.url` must be empty during validation (script should already be embedded)", i)
211211
}
212212
switch p.Mode {
213213
case ProvisionModeSystem, ProvisionModeUser, ProvisionModeBoot:
@@ -249,8 +249,8 @@ func Validate(y *LimaYAML, warn bool) error {
249249
}
250250
}
251251
for i, p := range y.Probes {
252-
if p.File != nil && *p.File != "" {
253-
return fmt.Errorf("field `probes[%d].file` must be empty during validation (script should already be embedded)", i)
252+
if p.File != nil && p.File.URL != "" {
253+
return fmt.Errorf("field `probes[%d].file.url` must be empty during validation (script should already be embedded)", i)
254254
}
255255
if !strings.HasPrefix(p.Script, "#!") {
256256
return fmt.Errorf("field `probe[%d].script` must start with a '#!' line", i)

templates/default.yaml

+11-2
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ containerd:
214214
#
215215
# EXPERIMENTAL Alternatively the script can be provided using the "file" property. This file is read when the instance
216216
# is created and then stored under the "script" property. When "file" is specified "script" must be empty.
217+
# The "file" property can either be a string (URL), or an object with a "url" and "digest" properties.
218+
# The "digest" property is currently unused.
217219
# Relative script files will be resolved relative to the location of the template file.
218220
# 🟢 Builtin default: []
219221
# provision:
@@ -226,7 +228,9 @@ containerd:
226228
# apt-get install -y vim
227229
# # `user` is executed without root privileges
228230
# - mode: user
229-
# file: user-provisioning.sh
231+
# file:
232+
# url: user-provisioning.sh
233+
# digest: deadbeef
230234
# # `boot` is executed directly by /bin/sh as part of cloud-init-local.service's early boot process,
231235
# # which is why there is no hash-bang specified in the example
232236
# # See cloud-init docs for more info https://docs.cloud-init.io/en/latest/reference/examples.html#run-commands-on-first-boot
@@ -255,6 +259,8 @@ containerd:
255259
# The scripts can use the following template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.
256260
# EXPERIMENTAL Alternatively the script can be provided using the "file" property. This file is read when the instance
257261
# is created and then stored under the "script" property. When "file" is specified "script" must be empty.
262+
# The "file" property can either be a string (URL), or an object with a "url" and "digest" properties.
263+
# The "digest" property is currently unused.
258264
# Relative script files will be resolved relative to the location of the template file.
259265
# 🟢 Builtin default: []
260266
# probes:
@@ -284,7 +290,10 @@ minimumLimaVersion: null
284290
# EXPERIMENTAL
285291
# Default settings can be imported from base templates. These will be merged in when the instance
286292
# is created, and the combined template is stored in the instance directory.
287-
# This setting ca be either a single string, or a list of strings.
293+
# This setting ca be either a single string (URL), or a list of locators.
294+
# A locator is again either a string (URL), or an object with "url" and "digest" properties, e.g.
295+
# base: [{url: ./base.yaml, digest: decafbad}, …]
296+
# The "digest" property is currently unused.
288297
# Any relative base template name will be resolved relative to the location of the main template.
289298
# 🟢 Builtin default: no base template
290299
base: null

0 commit comments

Comments
 (0)