Skip to content

Commit d7121f2

Browse files
committed
limactl: add --param shortcut for template parameters
Add a --param NAME=VALUE option to the shared edit flag handling used by limactl create, start, edit, clone, and rename. The option expands to the equivalent yq update for .param.NAME, so users can write --param release=v1.35 instead of --set '.param.release="v1.35"'. Reject params that are not already defined by the template/config, so typos fail before modifying the YAML. Add unit coverage for expression generation and BATS coverage for create and edit CLI behavior. Signed-off-by: Jan Dubois <jan.dubois@suse.com>
1 parent 0fa14f2 commit d7121f2

6 files changed

Lines changed: 149 additions & 6 deletions

File tree

cmd/limactl/clone.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func cloneOrRenameAction(cmd *cobra.Command, args []string) error {
7777
return err
7878
}
7979

80-
yqExprs, err := editflags.YQExpressions(flags, false)
80+
yqExprs, err := editflags.YQExpressions(flags, false, newInst.Config.Param)
8181
if err != nil {
8282
return err
8383
}

cmd/limactl/edit.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,15 @@ func editAction(cmd *cobra.Command, args []string) error {
8585
if err != nil {
8686
return err
8787
}
88-
yqExprs, err := editflags.YQExpressions(flags, false)
88+
var params map[string]string
89+
if flags.Changed("param") {
90+
var y limatype.LimaYAML
91+
if err := limayaml.Unmarshal(yContent, &y, filePath); err != nil {
92+
return err
93+
}
94+
params = y.Param
95+
}
96+
yqExprs, err := editflags.YQExpressions(flags, false, params)
8997
if err != nil {
9098
return err
9199
}

cmd/limactl/editflags/editflags.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func RegisterEdit(cmd *cobra.Command, commentPrefix string) {
6969
flags.Bool("rosetta", false, commentPrefix+"Enable Rosetta (for vz instances)")
7070

7171
flags.StringArray("set", []string{}, commentPrefix+"Modify the template inplace, using yq syntax. Can be passed multiple times.")
72+
flags.StringArray("param", []string{}, commentPrefix+"Set a template parameter, e.g. name=value. Can be passed multiple times.")
7273

7374
flags.Uint16("ssh-port", 0, commentPrefix+"SSH port (0 for random)") // colima-compatible
7475
_ = cmd.RegisterFlagCompletionFunc("ssh-port", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
@@ -169,6 +170,21 @@ func BuildPortForwardExpression(portForwards []string) (string, error) {
169170
return expr, nil
170171
}
171172

173+
func BuildParamExpressions(params []string, allowedParams map[string]string) ([]string, error) {
174+
exprs := make([]string, len(params))
175+
for i, param := range params {
176+
key, value, ok := strings.Cut(param, "=")
177+
if !ok {
178+
return nil, fmt.Errorf("invalid parameter %q, expected NAME=VALUE", param)
179+
}
180+
if _, ok := allowedParams[key]; !ok {
181+
return nil, fmt.Errorf("template does not define param %q", key)
182+
}
183+
exprs[i] = fmt.Sprintf(".param[%q] = %q", key, value)
184+
}
185+
return exprs, nil
186+
}
187+
172188
func buildMountListExpression(ss []string) (string, error) {
173189
mounts := make([]string, len(ss))
174190
for i, s := range ss {
@@ -184,7 +200,7 @@ func buildMountListExpression(ss []string) (string, error) {
184200
}
185201

186202
// YQExpressions returns YQ expressions.
187-
func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
203+
func YQExpressions(flags *flag.FlagSet, newInstance bool, params map[string]string) ([]string, error) {
188204
type def struct {
189205
flagName string
190206
exprFunc func(*flag.Flag) ([]string, error)
@@ -335,6 +351,13 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
335351
{"set", func(v *flag.Flag) ([]string, error) {
336352
return v.Value.(flag.SliceValue).GetSlice(), nil
337353
}, false, false},
354+
{"param", func(_ *flag.Flag) ([]string, error) {
355+
ss, err := flags.GetStringArray("param")
356+
if err != nil {
357+
return nil, err
358+
}
359+
return BuildParamExpressions(ss, params)
360+
}, false, false},
338361
{
339362
"video",
340363
func(_ *flag.Flag) ([]string, error) {

cmd/limactl/editflags/editflags_test.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,53 @@ func TestParsePortForward(t *testing.T) {
162162
}
163163
}
164164

165+
func TestBuildParamExpressions(t *testing.T) {
166+
tests := []struct {
167+
name string
168+
params []string
169+
allowedParams map[string]string
170+
expected []string
171+
expectError string
172+
}{
173+
{
174+
name: "single param",
175+
params: []string{"version=v1.35"},
176+
allowedParams: map[string]string{"version": ""},
177+
expected: []string{`.param["version"] = "v1.35"`},
178+
},
179+
{
180+
name: "value contains equals",
181+
params: []string{"token=a=b"},
182+
allowedParams: map[string]string{"token": ""},
183+
expected: []string{`.param["token"] = "a=b"`},
184+
},
185+
{
186+
name: "invalid format",
187+
params: []string{"version"},
188+
allowedParams: map[string]string{"version": ""},
189+
expectError: `invalid parameter "version", expected NAME=VALUE`,
190+
},
191+
{
192+
name: "undefined param",
193+
params: []string{"missing=value"},
194+
allowedParams: map[string]string{"version": ""},
195+
expectError: `template does not define param "missing"`,
196+
},
197+
}
198+
199+
for _, tt := range tests {
200+
t.Run(tt.name, func(t *testing.T) {
201+
result, err := BuildParamExpressions(tt.params, tt.allowedParams)
202+
if tt.expectError != "" {
203+
assert.ErrorContains(t, err, tt.expectError)
204+
} else {
205+
assert.NilError(t, err)
206+
assert.DeepEqual(t, tt.expected, result)
207+
}
208+
})
209+
}
210+
}
211+
165212
func TestYQExpressions(t *testing.T) {
166213
expand := func(s string) string {
167214
s, err := localpathutil.Expand(s)
@@ -225,6 +272,18 @@ func TestYQExpressions(t *testing.T) {
225272
newInstance: false,
226273
expected: []string{`.nestedVirtualization = true`},
227274
},
275+
{
276+
name: "param",
277+
args: []string{"--param", "version=v1.35"},
278+
newInstance: false,
279+
expected: []string{`.param["version"] = "v1.35"`},
280+
},
281+
{
282+
name: "undefined param",
283+
args: []string{"--param", "missing=value"},
284+
newInstance: false,
285+
expectError: `template does not define param "missing"`,
286+
},
228287
{
229288
name: "invalid network",
230289
args: []string{"--network", "invalid"},
@@ -237,7 +296,7 @@ func TestYQExpressions(t *testing.T) {
237296
cmd := &cobra.Command{}
238297
RegisterEdit(cmd, "")
239298
assert.NilError(t, cmd.ParseFlags(tt.args))
240-
expr, err := YQExpressions(cmd.Flags(), tt.newInstance)
299+
expr, err := YQExpressions(cmd.Flags(), tt.newInstance, map[string]string{"version": ""})
241300
if tt.expectError != "" {
242301
assert.ErrorContains(t, err, tt.expectError)
243302
} else {

cmd/limactl/start.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ func newCreateCommand() *cobra.Command {
5858
To create an instance "default" with yq expressions:
5959
$ limactl create --set='.cpus = 2 | .memory = "2GiB"'
6060
61+
To create an instance "default" with a template parameter:
62+
$ limactl create --name=default --param containerdSnapshotter=false template:docker
63+
6164
To see the template list:
6265
$ limactl create --list-templates
6366
@@ -282,7 +285,7 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
282285
return nil, fmt.Errorf("instance %q already exists", tmpl.Name)
283286
}
284287
logrus.Infof("Using the existing instance %q", tmpl.Name)
285-
yqExprs, err := editflags.YQExpressions(flags, false)
288+
yqExprs, err := editflags.YQExpressions(flags, false, inst.Config.Param)
286289
if err != nil {
287290
return nil, err
288291
}
@@ -331,7 +334,7 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
331334
if tmpl.Config != nil && tmpl.Config.OS != nil && *tmpl.Config.OS != limatype.LINUX {
332335
logrus.Warn("Support for non-Linux guests is experimental")
333336
}
334-
yqExprs, err := editflags.YQExpressions(flags, true)
337+
yqExprs, err := editflags.YQExpressions(flags, true, tmpl.Config.Param)
335338
if err != nil {
336339
return nil, err
337340
}

hack/bats/tests/param.bats

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# SPDX-FileCopyrightText: Copyright The Lima Authors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
load "../helpers/load"
5+
6+
LOCAL_LIMA_HOME="${LIMA_HOME:?}/_bats_param"
7+
8+
local_setup_file() {
9+
export LIMA_HOME="${LOCAL_LIMA_HOME:?}"
10+
rm -rf "${LOCAL_LIMA_HOME:?}"
11+
}
12+
13+
local_setup() {
14+
export LIMA_HOME="${LOCAL_LIMA_HOME:?}"
15+
}
16+
17+
param_template() {
18+
cat <<'EOF'
19+
images:
20+
- location: /etc/profile
21+
plain: true
22+
param:
23+
release: ""
24+
provision:
25+
- mode: system
26+
script: |
27+
echo "$PARAM_release"
28+
EOF
29+
}
30+
31+
@test 'create accepts --param shortcut' {
32+
run -0 limactl create --name param-create --param=release=v1.35 - <<<"$(param_template)"
33+
34+
run -0 limactl yq -r .param.release <"${LIMA_HOME}/param-create/lima.yaml"
35+
assert_output "v1.35"
36+
}
37+
38+
@test 'create rejects undefined --param' {
39+
run_e -1 limactl create --name param-create-invalid --param missing=value - <<<"$(param_template)"
40+
assert_fatal 'error while processing flag "param": template does not define param "missing"'
41+
}
42+
43+
@test 'edit accepts --param shortcut' {
44+
run -0 limactl create --name param-edit - <<<"$(param_template)"
45+
46+
run -0 limactl edit --param release=v1.36 param-edit
47+
48+
run -0 limactl yq -r .param.release <"${LIMA_HOME}/param-edit/lima.yaml"
49+
assert_output "v1.36"
50+
}

0 commit comments

Comments
 (0)