Skip to content

Commit

Permalink
Add optional support for specifying digests for template locators
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
jandubois committed Feb 10, 2025
1 parent 253c7ca commit b029e31
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 41 deletions.
8 changes: 4 additions & 4 deletions pkg/limatmpl/abs.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (tmpl *Template) useAbsLocators() error {
return err
}
for i, baseLocator := range tmpl.Config.Base {
locator, err := absPath(baseLocator, basePath)
locator, err := absPath(baseLocator.URL, basePath)
if err != nil {
return err
}
Expand All @@ -40,7 +40,7 @@ func (tmpl *Template) useAbsLocators() error {
}
for i, p := range tmpl.Config.Probes {
if p.File != nil {
locator, err := absPath(*p.File, basePath)
locator, err := absPath(p.File.URL, basePath)
if err != nil {
return err
}
Expand All @@ -49,7 +49,7 @@ func (tmpl *Template) useAbsLocators() error {
}
for i, p := range tmpl.Config.Provision {
if p.File != nil {
locator, err := absPath(*p.File, basePath)
locator, err := absPath(p.File.URL, basePath)
if err != nil {
return err
}
Expand All @@ -63,7 +63,7 @@ func (tmpl *Template) useAbsLocators() error {
// On Windows filepath.Abs() only returns a "rooted" name, but does not add the volume name.
// withVolume also normalizes all path separators to the platform native one.
func withVolume(path string) (string, error) {
if runtime.GOOS == "windows" && len(filepath.VolumeName(path)) == 0 {
if runtime.GOOS == "windows" && filepath.VolumeName(path) == "" {
root, err := filepath.Abs("/")
if err != nil {
return "", err
Expand Down
2 changes: 1 addition & 1 deletion pkg/limatmpl/abs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func TestAbsPath(t *testing.T) {
assert.ErrorContains(t, err, "volume")
})
}

t.Run("", func(t *testing.T) {
actual, err := absPath("foo", "template://")
assert.NilError(t, err)
Expand Down
30 changes: 16 additions & 14 deletions pkg/limatmpl/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sync"

"github.com/coreos/go-semver/semver"
"github.com/lima-vm/lima/pkg/limayaml"
"github.com/lima-vm/lima/pkg/store/dirnames"
"github.com/lima-vm/lima/pkg/store/filenames"
"github.com/lima-vm/lima/pkg/version/versionutil"
Expand All @@ -19,6 +20,7 @@ import (
var warnBaseIsExperimental = sync.OnceFunc(func() {
logrus.Warn("`base` is experimental")
})

var warnFileIsExperimental = sync.OnceFunc(func() {
logrus.Warn("`provision[*].file` and `probes[*].file` are experimental")
})
Expand Down Expand Up @@ -69,23 +71,23 @@ func (tmpl *Template) embedAllBases(ctx context.Context, embedAll, defaultBase b
break
}
baseLocator := tmpl.Config.Base[0]
isTemplate, _ := SeemsTemplateURL(baseLocator)
isTemplate, _ := SeemsTemplateURL(baseLocator.URL)
if isTemplate && !embedAll {
// Once we skip a template:// URL we can no longer embed any other base template
for i := 1; i < len(tmpl.Config.Base); i++ {
isTemplate, _ = SeemsTemplateURL(tmpl.Config.Base[i])
isTemplate, _ = SeemsTemplateURL(tmpl.Config.Base[i].URL)
if !isTemplate {
return fmt.Errorf("cannot embed template %q after not embedding %q", tmpl.Config.Base[i], baseLocator)
return fmt.Errorf("cannot embed template %q after not embedding %q", tmpl.Config.Base[i].URL, baseLocator.URL)
}
}
break
// TODO should we track embedding of template:// URLs so we can warn if we embed a non-template:// URL afterwards?
}

if seen[baseLocator] {
return fmt.Errorf("base template loop detected: template %q already included", baseLocator)
if seen[baseLocator.URL] {
return fmt.Errorf("base template loop detected: template %q already included", baseLocator.URL)
}
seen[baseLocator] = true
seen[baseLocator.URL] = true

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

func (tmpl *Template) embedBase(ctx context.Context, baseLocator string, embedAll bool, seen map[string]bool) error {
func (tmpl *Template) embedBase(ctx context.Context, baseLocator limayaml.LocatorWithDigest, embedAll bool, seen map[string]bool) error {
warnBaseIsExperimental()
logrus.Debugf("Embedding base %q in template %q", baseLocator, tmpl.Locator)
logrus.Debugf("Embedding base %q in template %q", baseLocator.URL, tmpl.Locator)
if err := tmpl.Unmarshal(); err != nil {
return err
}
base, err := Read(ctx, "", baseLocator)
base, err := Read(ctx, "", baseLocator.URL)
if err != nil {
return err
}
Expand Down Expand Up @@ -308,7 +310,7 @@ func (tmpl *Template) deleteListEntry(list string, idx int) {
tmpl.expr.WriteString(fmt.Sprintf("| del($a.%s[%d], $b.%s[%d])\n", list, idx, list, idx))
}

// upgradeListEntryStringToMapField turns list[idx] from a string to a {field: list[idx]} map
// upgradeListEntryStringToMapField turns list[idx] from a string to a {field: list[idx]} map.
func (tmpl *Template) upgradeListEntryStringToMapField(list string, idx int, field string) {
// TODO the head_comment on the string becomes duplicated as a foot_comment on the new field; could be a yq bug?
tmpl.expr.WriteString(fmt.Sprintf("| ($a.%s[%d] | select(type == \"!!str\")) |= {\"%s\": .}\n", list, idx, field))
Expand Down Expand Up @@ -557,9 +559,9 @@ func (tmpl *Template) embedAllScripts(ctx context.Context, embedAll bool) error
// Don't overwrite existing script. This should throw an error during validation.
if p.File != nil && p.Script == "" {
warnFileIsExperimental()
isTemplate, _ := SeemsTemplateURL(*p.File)
isTemplate, _ := SeemsTemplateURL(p.File.URL)
if embedAll || !isTemplate {
scriptTmpl, err := Read(ctx, "", *p.File)
scriptTmpl, err := Read(ctx, "", p.File.URL)
if err != nil {
return err
}
Expand All @@ -570,9 +572,9 @@ func (tmpl *Template) embedAllScripts(ctx context.Context, embedAll bool) error
for i, p := range tmpl.Config.Provision {
if p.File != nil && p.Script == "" {
warnFileIsExperimental()
isTemplate, _ := SeemsTemplateURL(*p.File)
isTemplate, _ := SeemsTemplateURL(p.File.URL)
if embedAll || !isTemplate {
scriptTmpl, err := Read(ctx, "", *p.File)
scriptTmpl, err := Read(ctx, "", p.File.URL)
if err != nil {
return err
}
Expand Down
11 changes: 7 additions & 4 deletions pkg/limatmpl/embed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ package limatmpl_test
import (
"context"
"fmt"
"github.com/sirupsen/logrus"
"os"
"reflect"
"strings"
"testing"

"github.com/lima-vm/lima/pkg/limatmpl"
"github.com/lima-vm/lima/pkg/limayaml"
"github.com/sirupsen/logrus"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
)
Expand Down Expand Up @@ -179,13 +179,16 @@ mounts:
},
{
"template:// URLs are not embedded when embedAll is false",
// also tests file.url format
``,
`
base: template://default
provision:
- file: template://provision.sh
- file:
url: template://provision.sh
probes:
- file: template://probe.sh
- file:
url: template://probe.sh
`,
`
base: template://default
Expand Down Expand Up @@ -228,7 +231,7 @@ base: baseX.yaml`,
"Bases are embedded depth-first",
`#`,
`
base: [base1.yaml, base2.yaml]
base: [base1.yaml, {url: base2.yaml}] # also test file.url format
additionalDisks: [disk0]
---
base: base3.yaml
Expand Down
2 changes: 1 addition & 1 deletion pkg/limatmpl/locator.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func Read(ctx context.Context, name, locator string) (*Template, error) {
return nil, err
}
}
logrus.Debugf("interpreting argument %q as a file url for instance %q", locator, tmpl.Name)
logrus.Debugf("interpreting argument %q as a file URL for instance %q", locator, tmpl.Name)
filePath := strings.TrimPrefix(locator, "file://")
if !filepath.IsAbs(filePath) {
return nil, fmt.Errorf("file URL %q is not an absolute path", locator)
Expand Down
27 changes: 16 additions & 11 deletions pkg/limayaml/limayaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ type LimaYAML struct {
User User `yaml:"user,omitempty" json:"user,omitempty"`
}

type BaseTemplates []string
type BaseTemplates []LocatorWithDigest

type LocatorWithDigest struct {
URL string `yaml:"url" json:"url"`
Digest *string `yaml:"digest,omitempty" json:"digest,omitempty" jsonschema:"nullable"` // TODO currently unused
}

type (
OS = string
Expand Down Expand Up @@ -215,11 +220,11 @@ const (
)

type Provision struct {
Mode ProvisionMode `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=system"`
SkipDefaultDependencyResolution *bool `yaml:"skipDefaultDependencyResolution,omitempty" json:"skipDefaultDependencyResolution,omitempty"`
Script string `yaml:"script" json:"script"`
File *string `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
Playbook string `yaml:"playbook,omitempty" json:"playbook,omitempty"`
Mode ProvisionMode `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=system"`
SkipDefaultDependencyResolution *bool `yaml:"skipDefaultDependencyResolution,omitempty" json:"skipDefaultDependencyResolution,omitempty"`
Script string `yaml:"script" json:"script"`
File *LocatorWithDigest `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
Playbook string `yaml:"playbook,omitempty" json:"playbook,omitempty"`
}

type Containerd struct {
Expand All @@ -235,11 +240,11 @@ const (
)

type Probe struct {
Mode ProbeMode `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=readiness"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Script string `yaml:"script,omitempty" json:"script,omitempty"`
File *string `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
Hint string `yaml:"hint,omitempty" json:"hint,omitempty"`
Mode ProbeMode `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=readiness"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Script string `yaml:"script,omitempty" json:"script,omitempty"`
File *LocatorWithDigest `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
Hint string `yaml:"hint,omitempty" json:"hint,omitempty"`
}

type Proto = string
Expand Down
15 changes: 13 additions & 2 deletions pkg/limayaml/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,21 @@ func unmarshalDisk(dst *Disk, b []byte) error {
return yaml.Unmarshal(b, dst)
}

// unmarshalBaseTemplates unmarshalls `base` which is either a string or a list of strings.
// unmarshalBaseTemplates unmarshalls `base` which is either a string or a list of Locators.
func unmarshalBaseTemplates(dst *BaseTemplates, b []byte) error {
var s string
if err := yaml.Unmarshal(b, &s); err == nil {
*dst = BaseTemplates{s}
*dst = BaseTemplates{LocatorWithDigest{URL: s}}
return nil
}
return yaml.UnmarshalWithOptions(b, dst, yaml.CustomUnmarshaler[LocatorWithDigest](unmarshalLocatorWithDigest))
}

// unmarshalLocator unmarshalls a locator which is either a string or a Locator struct.
func unmarshalLocatorWithDigest(dst *LocatorWithDigest, b []byte) error {
var s string
if err := yaml.Unmarshal(b, &s); err == nil {
*dst = LocatorWithDigest{URL: s}
return nil
}
return yaml.Unmarshal(b, dst)
Expand All @@ -49,6 +59,7 @@ func Unmarshal(data []byte, v any, comment string) error {
opts := []yaml.DecodeOption{
yaml.CustomUnmarshaler[BaseTemplates](unmarshalBaseTemplates),
yaml.CustomUnmarshaler[Disk](unmarshalDisk),
yaml.CustomUnmarshaler[LocatorWithDigest](unmarshalLocatorWithDigest),
}
if err := yaml.UnmarshalWithOptions(data, v, opts...); err != nil {
return fmt.Errorf("failed to unmarshal YAML (%s): %w", comment, err)
Expand Down
8 changes: 4 additions & 4 deletions pkg/limayaml/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ func Validate(y *LimaYAML, warn bool) error {
// y.Firmware.LegacyBIOS is ignored for aarch64, but not a fatal error.

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

0 comments on commit b029e31

Please sign in to comment.