Skip to content

Commit 8f143ae

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 bfac818 commit 8f143ae

File tree

14 files changed

+1353
-26
lines changed

14 files changed

+1353
-26
lines changed

cmd/limactl/start.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,9 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
205205
return nil, err
206206
}
207207
}
208-
208+
if err := tmpl.Embed(cmd.Context()); err != nil {
209+
return nil, err
210+
}
209211
yqExprs, err := editflags.YQExpressions(flags, true)
210212
if err != nil {
211213
return nil, err

cmd/limactl/template.go

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package main
55

66
import (
7+
"errors"
78
"fmt"
89
"os"
910
"path/filepath"
@@ -52,24 +53,74 @@ var templateCopyExample = ` Template locators are local files, file://, https:/
5253

5354
func newTemplateCopyCommand() *cobra.Command {
5455
templateCopyCommand := &cobra.Command{
55-
Use: "copy TEMPLATE DEST",
56+
Use: "copy [OPTIONS] TEMPLATE DEST",
5657
Short: "Copy template",
5758
Long: "Copy a template via locator to a local file",
5859
Example: templateCopyExample,
5960
Args: WrapArgsError(cobra.ExactArgs(2)),
6061
RunE: templateCopyAction,
6162
}
63+
templateCopyCommand.Flags().Bool("embed", false, "embed dependencies into template")
64+
templateCopyCommand.Flags().Bool("fill", false, "fill defaults")
65+
templateCopyCommand.Flags().Bool("verbatim", false, "don't make locators absolute")
6266
return templateCopyCommand
6367
}
6468

6569
func templateCopyAction(cmd *cobra.Command, args []string) error {
70+
embed, err := cmd.Flags().GetBool("embed")
71+
if err != nil {
72+
return err
73+
}
74+
fill, err := cmd.Flags().GetBool("fill")
75+
if err != nil {
76+
return err
77+
}
78+
verbatim, err := cmd.Flags().GetBool("verbatim")
79+
if err != nil {
80+
return err
81+
}
82+
if embed && verbatim {
83+
return errors.New("--embed and --verbatim cannot be used together")
84+
}
85+
if fill && verbatim {
86+
return errors.New("--fill and --verbatim cannot be used together")
87+
}
88+
6689
tmpl, err := limatmpl.Read(cmd.Context(), "", args[0])
6790
if err != nil {
6891
return err
6992
}
7093
if len(tmpl.Bytes) == 0 {
7194
return fmt.Errorf("don't know how to interpret %q as a template locator", args[0])
7295
}
96+
if !verbatim {
97+
if embed {
98+
if err := tmpl.Embed(cmd.Context()); err != nil {
99+
return err
100+
}
101+
} else {
102+
if err := tmpl.UseAbsLocators(); err != nil {
103+
return err
104+
}
105+
}
106+
}
107+
if fill {
108+
limaDir, err := dirnames.LimaDir()
109+
if err != nil {
110+
return err
111+
}
112+
// Load() will merge the template with override.yaml and default.yaml via FillDefaults().
113+
// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.
114+
filePath := filepath.Join(limaDir, tmpl.Name+".yaml")
115+
tmpl.Config, err = limayaml.Load(tmpl.Bytes, filePath)
116+
if err != nil {
117+
return err
118+
}
119+
tmpl.Bytes, err = limayaml.Marshal(tmpl.Config, false)
120+
if err != nil {
121+
return err
122+
}
123+
}
73124
writer := cmd.OutOrStdout()
74125
target := args[1]
75126
if target != "-" {
@@ -118,8 +169,8 @@ func templateValidateAction(cmd *cobra.Command, args []string) error {
118169
}
119170
// Load() will merge the template with override.yaml and default.yaml via FillDefaults().
120171
// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.
121-
instDir := filepath.Join(limaDir, tmpl.Name)
122-
y, err := limayaml.Load(tmpl.Bytes, instDir)
172+
filePath := filepath.Join(limaDir, tmpl.Name+".yaml")
173+
y, err := limayaml.Load(tmpl.Bytes, filePath)
123174
if err != nil {
124175
return err
125176
}

pkg/limatmpl/abs.go

Lines changed: 89 additions & 0 deletions
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

Lines changed: 199 additions & 0 deletions
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)