Skip to content

Commit 757d8f3

Browse files
committed
Assemble templates using the new basedOn setting
It allows a template to be constructed by merging values from one or more base templates together. This merge process will maintain all comments from both the template and the bases. The template is assembled before an instance is created, and only the combined template is stored as lima.yaml in the instance directory. There merging semantics are otherwise similar to how lima.yaml is combined with override.yaml, defaults.yaml, and the builtin default values. Signed-off-by: Jan Dubois <[email protected]>
1 parent 9be3b9a commit 757d8f3

File tree

14 files changed

+1353
-26
lines changed

14 files changed

+1353
-26
lines changed

cmd/limactl/start.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,9 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
196196
return nil, err
197197
}
198198
}
199-
199+
if err := tmpl.Embed(cmd.Context()); err != nil {
200+
return nil, err
201+
}
200202
yqExprs, err := editflags.YQExpressions(flags, true)
201203
if err != nil {
202204
return nil, err

cmd/limactl/template.go

+54-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"path/filepath"
@@ -49,24 +50,74 @@ var templateCopyExample = ` Template locators are local files, file://, https:/
4950

5051
func newTemplateCopyCommand() *cobra.Command {
5152
templateCopyCommand := &cobra.Command{
52-
Use: "copy TEMPLATE DEST",
53+
Use: "copy [OPTIONS] TEMPLATE DEST",
5354
Short: "Copy template",
5455
Long: "Copy a template via locator to a local file",
5556
Example: templateCopyExample,
5657
Args: WrapArgsError(cobra.ExactArgs(2)),
5758
RunE: templateCopyAction,
5859
}
60+
templateCopyCommand.Flags().Bool("embed", false, "embed dependencies into template")
61+
templateCopyCommand.Flags().Bool("fill", false, "fill defaults")
62+
templateCopyCommand.Flags().Bool("verbatim", false, "don't make locators absolute")
5963
return templateCopyCommand
6064
}
6165

6266
func templateCopyAction(cmd *cobra.Command, args []string) error {
67+
embed, err := cmd.Flags().GetBool("embed")
68+
if err != nil {
69+
return err
70+
}
71+
fill, err := cmd.Flags().GetBool("fill")
72+
if err != nil {
73+
return err
74+
}
75+
verbatim, err := cmd.Flags().GetBool("verbatim")
76+
if err != nil {
77+
return err
78+
}
79+
if embed && verbatim {
80+
return errors.New("--embed and --verbatim cannot be used together")
81+
}
82+
if fill && verbatim {
83+
return errors.New("--fill and --verbatim cannot be used together")
84+
}
85+
6386
tmpl, err := limatmpl.Read(cmd.Context(), "", args[0])
6487
if err != nil {
6588
return err
6689
}
6790
if len(tmpl.Bytes) == 0 {
6891
return fmt.Errorf("don't know how to interpret %q as a template locator", args[0])
6992
}
93+
if !verbatim {
94+
if embed {
95+
if err := tmpl.Embed(cmd.Context()); err != nil {
96+
return err
97+
}
98+
} else {
99+
if err := tmpl.UseAbsLocators(); err != nil {
100+
return err
101+
}
102+
}
103+
}
104+
if fill {
105+
limaDir, err := dirnames.LimaDir()
106+
if err != nil {
107+
return err
108+
}
109+
// Load() will merge the template with override.yaml and default.yaml via FillDefaults().
110+
// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.
111+
filePath := filepath.Join(limaDir, tmpl.Name+".yaml")
112+
tmpl.Config, err = limayaml.Load(tmpl.Bytes, filePath)
113+
if err != nil {
114+
return err
115+
}
116+
tmpl.Bytes, err = limayaml.Marshal(tmpl.Config, false)
117+
if err != nil {
118+
return err
119+
}
120+
}
70121
writer := cmd.OutOrStdout()
71122
target := args[1]
72123
if target != "-" {
@@ -115,8 +166,8 @@ func templateValidateAction(cmd *cobra.Command, args []string) error {
115166
}
116167
// Load() will merge the template with override.yaml and default.yaml via FillDefaults().
117168
// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.
118-
instDir := filepath.Join(limaDir, tmpl.Name)
119-
y, err := limayaml.Load(tmpl.Bytes, instDir)
169+
filePath := filepath.Join(limaDir, tmpl.Name+".yaml")
170+
y, err := limayaml.Load(tmpl.Bytes, filePath)
120171
if err != nil {
121172
return err
122173
}

pkg/limatmpl/abs.go

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package limatmpl
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/url"
7+
"path/filepath"
8+
"strings"
9+
)
10+
11+
// UseAbsLocators will replace all relative template locators with absolute ones, so this template
12+
// can be stored anywhere and still reference the same base templates and files.
13+
func (tmpl *Template) UseAbsLocators() error {
14+
err := tmpl.useAbsLocators()
15+
return tmpl.ClearOnError(err)
16+
}
17+
18+
func (tmpl *Template) useAbsLocators() error {
19+
if err := tmpl.Unmarshal(); err != nil {
20+
return err
21+
}
22+
basePath, err := basePath(tmpl.Locator)
23+
if err != nil {
24+
return err
25+
}
26+
for i, baseLocator := range tmpl.Config.BasedOn {
27+
locator, err := absPath(baseLocator, basePath)
28+
if err != nil {
29+
return err
30+
}
31+
if i == 0 {
32+
// basedOn can either be a single string, or a list of strings
33+
tmpl.expr.WriteString(fmt.Sprintf("| ($a.basedOn | select(type == \"!!str\")) |= %q\n", locator))
34+
tmpl.expr.WriteString(fmt.Sprintf("| ($a.basedOn | select(type == \"!!seq\") | .[0]) |= %q\n", locator))
35+
} else {
36+
tmpl.expr.WriteString(fmt.Sprintf("| $a.basedOn[%d] = %q\n", i, locator))
37+
}
38+
}
39+
for i, p := range tmpl.Config.Probes {
40+
if p.File != nil {
41+
locator, err := absPath(*p.File, basePath)
42+
if err != nil {
43+
return err
44+
}
45+
tmpl.expr.WriteString(fmt.Sprintf("| $a.probes[%d].file = %q\n", i, locator))
46+
}
47+
}
48+
for i, p := range tmpl.Config.Provision {
49+
if p.File != nil {
50+
locator, err := absPath(*p.File, basePath)
51+
if err != nil {
52+
return err
53+
}
54+
tmpl.expr.WriteString(fmt.Sprintf("| $a.provision[%d].file = %q\n", i, locator))
55+
}
56+
}
57+
return tmpl.evalExpr()
58+
}
59+
60+
// basePath returns the locator without the filename part.
61+
func basePath(locator string) (string, error) {
62+
u, err := url.Parse(locator)
63+
if err != nil || u.Scheme == "" {
64+
return filepath.Abs(filepath.Dir(locator))
65+
}
66+
// filepath.Dir("") returns ".", which must be removed for url.JoinPath() to do the right thing later
67+
return u.Scheme + "://" + strings.TrimSuffix(filepath.Dir(filepath.Join(u.Host, u.Path)), "."), nil
68+
}
69+
70+
// absPath either returns the locator directly, or combines it with the basePath if the locator is a relative path.
71+
func absPath(locator, basePath string) (string, error) {
72+
u, err := url.Parse(locator)
73+
if (err == nil && u.Scheme != "") || filepath.IsAbs(locator) {
74+
return locator, nil
75+
}
76+
switch {
77+
case basePath == "":
78+
return "", errors.New("basePath is empty")
79+
case basePath == "-":
80+
return "", errors.New("can't use relative paths when reading template from STDIN")
81+
case strings.Contains(locator, "../"):
82+
return "", fmt.Errorf("relative locator path %q must not contain '../' segments", locator)
83+
}
84+
u, err = url.Parse(basePath)
85+
if err != nil {
86+
return "", err
87+
}
88+
return u.JoinPath(locator).String(), nil
89+
}

pkg/limatmpl/abs_test.go

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package limatmpl
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"gotest.tools/v3/assert"
8+
)
9+
10+
type useAbsLocatorsTestCase struct {
11+
description string
12+
locator string
13+
template string
14+
expected string
15+
}
16+
17+
var useAbsLocatorsTestCases = []useAbsLocatorsTestCase{
18+
{
19+
"Template without basedOn or script file",
20+
"template://foo",
21+
`foo: bar`,
22+
`foo: bar`,
23+
},
24+
{
25+
"Single string base template",
26+
"template://foo",
27+
`basedOn: bar.yaml`,
28+
`basedOn: template://bar.yaml`,
29+
},
30+
{
31+
"Flow style array of one base template",
32+
"template://foo",
33+
`basedOn: [bar.yaml]`,
34+
`basedOn: ['template://bar.yaml']`,
35+
},
36+
{
37+
"Block style array of one base template",
38+
"template://foo",
39+
`
40+
basedOn:
41+
- bar.yaml
42+
`,
43+
`
44+
basedOn:
45+
- template://bar.yaml`,
46+
},
47+
{
48+
"Block style of four base templates",
49+
"template://foo",
50+
`
51+
basedOn:
52+
- bar.yaml
53+
- template://my
54+
- https://example.com/my.yaml
55+
- baz.yaml
56+
`,
57+
`
58+
basedOn:
59+
- template://bar.yaml
60+
- template://my
61+
- https://example.com/my.yaml
62+
- template://baz.yaml
63+
`,
64+
},
65+
{
66+
"Provisioning and probe scripts",
67+
"template://experimental/foo",
68+
`
69+
provision:
70+
- mode: user
71+
file: script.sh
72+
probes:
73+
- file: probe.sh
74+
`,
75+
`
76+
provision:
77+
- mode: user
78+
file: template://experimental/script.sh
79+
probes:
80+
- file: template://experimental/probe.sh
81+
`,
82+
},
83+
}
84+
85+
func TestUseAbsLocators(t *testing.T) {
86+
for _, tc := range useAbsLocatorsTestCases {
87+
t.Run(tc.description, func(t *testing.T) { RunUseAbsLocatorTest(t, tc) })
88+
}
89+
}
90+
91+
func RunUseAbsLocatorTest(t *testing.T, tc useAbsLocatorsTestCase) {
92+
tmpl := &Template{
93+
Bytes: []byte(strings.TrimSpace(tc.template)),
94+
Locator: tc.locator,
95+
}
96+
err := tmpl.UseAbsLocators()
97+
assert.NilError(t, err, tc.description)
98+
99+
actual := strings.TrimSpace(string(tmpl.Bytes))
100+
expected := strings.TrimSpace(tc.expected)
101+
assert.Equal(t, actual, expected, tc.description)
102+
}
103+
104+
func TestBasePath(t *testing.T) {
105+
actual, err := basePath("/foo")
106+
assert.NilError(t, err)
107+
assert.Equal(t, actual, "/")
108+
109+
actual, err = basePath("/foo/bar")
110+
assert.NilError(t, err)
111+
assert.Equal(t, actual, "/foo")
112+
113+
actual, err = basePath("template://foo")
114+
assert.NilError(t, err)
115+
assert.Equal(t, actual, "template://")
116+
117+
actual, err = basePath("template://foo/bar")
118+
assert.NilError(t, err)
119+
assert.Equal(t, actual, "template://foo")
120+
121+
actual, err = basePath("http://host/foo")
122+
assert.NilError(t, err)
123+
assert.Equal(t, actual, "http://host")
124+
125+
actual, err = basePath("http://host/foo/bar")
126+
assert.NilError(t, err)
127+
assert.Equal(t, actual, "http://host/foo")
128+
129+
actual, err = basePath("file:///foo")
130+
assert.NilError(t, err)
131+
assert.Equal(t, actual, "file:///")
132+
133+
actual, err = basePath("file:///foo/bar")
134+
assert.NilError(t, err)
135+
assert.Equal(t, actual, "file:///foo")
136+
}
137+
138+
func TestAbsPath(t *testing.T) {
139+
// If the locator is already an absolute path, it is returned unchanged (no extension appended either)
140+
actual, err := absPath("/foo", "/root")
141+
assert.NilError(t, err)
142+
assert.Equal(t, actual, "/foo")
143+
144+
actual, err = absPath("template://foo", "/root")
145+
assert.NilError(t, err)
146+
assert.Equal(t, actual, "template://foo")
147+
148+
actual, err = absPath("http://host/foo", "/root")
149+
assert.NilError(t, err)
150+
assert.Equal(t, actual, "http://host/foo")
151+
152+
actual, err = absPath("file:///foo", "/root")
153+
assert.NilError(t, err)
154+
assert.Equal(t, actual, "file:///foo")
155+
156+
// Can't have relative path when reading from STDIN
157+
_, err = absPath("foo", "-")
158+
assert.ErrorContains(t, err, "STDIN")
159+
160+
// Relative paths must be underneath the basePath
161+
_, err = absPath("../foo", "/root")
162+
assert.ErrorContains(t, err, "'../'")
163+
164+
// basePath must not be empty
165+
_, err = absPath("foo", "")
166+
assert.ErrorContains(t, err, "empty")
167+
168+
_, err = absPath("./foo", "")
169+
assert.ErrorContains(t, err, "empty")
170+
171+
// Check relative paths with all the supported schemes
172+
actual, err = absPath("./foo", "/root")
173+
assert.NilError(t, err)
174+
assert.Equal(t, actual, "/root/foo")
175+
176+
actual, err = absPath("foo", "template://")
177+
assert.NilError(t, err)
178+
assert.Equal(t, actual, "template://foo")
179+
180+
actual, err = absPath("bar", "template://foo")
181+
assert.NilError(t, err)
182+
assert.Equal(t, actual, "template://foo/bar")
183+
184+
actual, err = absPath("foo", "http://host")
185+
assert.NilError(t, err)
186+
assert.Equal(t, actual, "http://host/foo")
187+
188+
actual, err = absPath("bar", "http://host/foo")
189+
assert.NilError(t, err)
190+
assert.Equal(t, actual, "http://host/foo/bar")
191+
192+
actual, err = absPath("foo", "file:///")
193+
assert.NilError(t, err)
194+
assert.Equal(t, actual, "file:///foo")
195+
196+
actual, err = absPath("bar", "file:///foo")
197+
assert.NilError(t, err)
198+
assert.Equal(t, actual, "file:///foo/bar")
199+
}

0 commit comments

Comments
 (0)