Skip to content

Commit bd7048f

Browse files
authored
Merge pull request #1468 from AkihiroSuda/edit-flags
limactl: add `--cpus`, `--memory`, `--mount-type`, `--vm-type`, ...
2 parents f682bd5 + 3b8e0ce commit bd7048f

File tree

7 files changed

+300
-13
lines changed

7 files changed

+300
-13
lines changed

cmd/limactl/edit.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
"fmt"
77
"os"
88
"path/filepath"
9+
"strings"
910

1011
"github.com/AlecAivazis/survey/v2"
12+
"github.com/lima-vm/lima/cmd/limactl/editflags"
1113
"github.com/lima-vm/lima/pkg/editutil"
1214
"github.com/lima-vm/lima/pkg/limayaml"
1315
networks "github.com/lima-vm/lima/pkg/networks/reconcile"
@@ -30,7 +32,7 @@ func newEditCommand() *cobra.Command {
3032
}
3133
// TODO: "survey" does not support using cygwin terminal on windows yet
3234
editCommand.Flags().Bool("tty", isatty.IsTerminal(os.Stdout.Fd()), "enable TUI interactions such as opening an editor, defaults to true when stdout is a terminal")
33-
editCommand.Flags().String("set", "", "modify the template inplace, using yq syntax")
35+
editflags.RegisterEdit(editCommand)
3436
return editCommand
3537
}
3638

@@ -57,17 +59,18 @@ func editAction(cmd *cobra.Command, args []string) error {
5759
if err != nil {
5860
return err
5961
}
60-
tty, err := cmd.Flags().GetBool("tty")
62+
flags := cmd.Flags()
63+
tty, err := flags.GetBool("tty")
6164
if err != nil {
6265
return err
6366
}
64-
yq, err := cmd.Flags().GetString("set")
67+
yqExprs, err := editflags.YQExpressions(flags)
6568
if err != nil {
6669
return err
6770
}
6871
var yBytes []byte
69-
if yq != "" {
70-
logrus.Warn("`--set` is experimental")
72+
if len(yqExprs) > 0 {
73+
yq := strings.Join(yqExprs, " | ")
7174
yBytes, err = yqutil.EvaluateExpression(yq, yContent)
7275
if err != nil {
7376
return err

cmd/limactl/editflags/editflags.go

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package editflags
2+
3+
import (
4+
"fmt"
5+
"math/bits"
6+
"runtime"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/pbnjay/memory"
11+
"github.com/sirupsen/logrus"
12+
"github.com/spf13/cobra"
13+
flag "github.com/spf13/pflag"
14+
)
15+
16+
// RegisterEdit registers flags related to in-place YAML modification, for `limactl edit`.
17+
func RegisterEdit(cmd *cobra.Command) {
18+
registerEdit(cmd, "")
19+
}
20+
21+
func registerEdit(cmd *cobra.Command, commentPrefix string) {
22+
flags := cmd.Flags()
23+
24+
flags.Int("cpus", 0, commentPrefix+"number of CPUs") // Similar to colima's --cpu, but the flag name is slightly different (cpu vs cpus)
25+
_ = cmd.RegisterFlagCompletionFunc("cpus", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
26+
var res []string
27+
for _, f := range completeCPUs(runtime.NumCPU()) {
28+
res = append(res, strconv.Itoa(f))
29+
}
30+
return res, cobra.ShellCompDirectiveNoFileComp
31+
})
32+
33+
flags.IPSlice("dns", nil, commentPrefix+"specify custom DNS (disable host resolver)") // colima-compatible
34+
35+
flags.Float32("memory", 0, commentPrefix+"memory in GiB") // colima-compatible
36+
_ = cmd.RegisterFlagCompletionFunc("memory", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
37+
var res []string
38+
for _, f := range completeMemoryGiB(memory.TotalMemory()) {
39+
res = append(res, fmt.Sprintf("%.1f", f))
40+
}
41+
return res, cobra.ShellCompDirectiveNoFileComp
42+
})
43+
44+
flags.StringSlice("mount", nil, commentPrefix+"directories to mount, suffix ':w' for writable (Do not specify directories that overlap with the existing mounts)") // colima-compatible
45+
46+
flags.String("mount-type", "", commentPrefix+"mount type (reverse-sshfs, 9p, virtiofs)") // Similar to colima's --mount-type=(sshfs|9p|virtiofs), but "reverse-sshfs" is Lima is called "sshfs" in colima
47+
_ = cmd.RegisterFlagCompletionFunc("mount-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
48+
return []string{"reverse-sshfs", "9p", "virtiofs"}, cobra.ShellCompDirectiveNoFileComp
49+
})
50+
51+
flags.Bool("mount-writable", false, commentPrefix+"make all mounts writable")
52+
53+
flags.StringSlice("network", nil, commentPrefix+"additional networks, e.g., \"vzNAT\" or \"lima:shared\" to assign vmnet IP")
54+
_ = cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
55+
// TODO: retrieve the lima:* network list from networks.yaml
56+
return []string{"lima:shared", "lima:bridged", "lima:host", "lima:user-v2", "vzNAT"}, cobra.ShellCompDirectiveNoFileComp
57+
})
58+
59+
flags.Bool("rosetta", false, commentPrefix+"enable Rosetta (for vz instances)")
60+
61+
flags.String("set", "", commentPrefix+"modify the template inplace, using yq syntax")
62+
}
63+
64+
// RegisterCreate registers flags related to in-place YAML modification, for `limactl create`.
65+
func RegisterCreate(cmd *cobra.Command, commentPrefix string) {
66+
registerEdit(cmd, commentPrefix)
67+
flags := cmd.Flags()
68+
69+
flags.String("arch", "", commentPrefix+"machine architecture (x86_64, aarch64, riscv64)") // colima-compatible
70+
_ = cmd.RegisterFlagCompletionFunc("arch", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
71+
return []string{"x86_64", "aarch64", "riscv64"}, cobra.ShellCompDirectiveNoFileComp
72+
})
73+
74+
flags.String("containerd", "", commentPrefix+"containerd mode (user, system, user+system, none)")
75+
_ = cmd.RegisterFlagCompletionFunc("vm-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
76+
return []string{"user", "system", "user+system", "none"}, cobra.ShellCompDirectiveNoFileComp
77+
})
78+
79+
flags.Float32("disk", 0, commentPrefix+"disk size in GiB") // colima-compatible
80+
_ = cmd.RegisterFlagCompletionFunc("memory", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
81+
return []string{"10", "30", "50", "100", "200"}, cobra.ShellCompDirectiveNoFileComp
82+
})
83+
84+
flags.String("vm-type", "", commentPrefix+"virtual machine type (qemu, vz)") // colima-compatible
85+
_ = cmd.RegisterFlagCompletionFunc("vm-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
86+
return []string{"qemu", "vz"}, cobra.ShellCompDirectiveNoFileComp
87+
})
88+
}
89+
90+
func defaultExprFunc(expr string) func(v *flag.Flag) (string, error) {
91+
return func(v *flag.Flag) (string, error) {
92+
return fmt.Sprintf(expr, v.Value), nil
93+
}
94+
}
95+
96+
// YQExpressions returns YQ expressions.
97+
func YQExpressions(flags *flag.FlagSet) ([]string, error) {
98+
type def struct {
99+
flagName string
100+
exprFunc func(*flag.Flag) (string, error)
101+
experimental bool
102+
}
103+
d := defaultExprFunc
104+
defs := []def{
105+
{"cpus", d(".cpus = %s"), false},
106+
{"dns",
107+
func(_ *flag.Flag) (string, error) {
108+
ipSlice, err := flags.GetIPSlice("dns")
109+
if err != nil {
110+
return "", err
111+
}
112+
expr := `.dns += [`
113+
for i, ip := range ipSlice {
114+
expr += fmt.Sprintf("%q", ip)
115+
if i < len(ipSlice)-1 {
116+
expr += ","
117+
}
118+
}
119+
expr += `] | .dns |= unique | .hostResolver.enabled=false`
120+
logrus.Warnf("Disabling HostResolver, as custom DNS addresses (%v) are specified", ipSlice)
121+
return expr, nil
122+
},
123+
false},
124+
{"memory", d(".memory = \"%sGiB\""), false},
125+
{"mount",
126+
func(_ *flag.Flag) (string, error) {
127+
ss, err := flags.GetStringSlice("mount")
128+
if err != nil {
129+
return "", err
130+
}
131+
expr := `.mounts += [`
132+
for i, s := range ss {
133+
writable := strings.HasSuffix(s, ":w")
134+
loc := strings.TrimSuffix(s, ":w")
135+
expr += fmt.Sprintf(`{"location": %q, "writable": %v}`, loc, writable)
136+
if i < len(ss)-1 {
137+
expr += ","
138+
}
139+
}
140+
expr += `] | .mounts |= unique_by(.location)`
141+
return expr, nil
142+
},
143+
false},
144+
{"mount-type", d(".mountType = %q"), false},
145+
{"mount-writable", d(".mounts[].writable = %s"), false},
146+
{"network",
147+
func(_ *flag.Flag) (string, error) {
148+
ss, err := flags.GetStringSlice("network")
149+
if err != nil {
150+
return "", err
151+
}
152+
expr := `.networks += [`
153+
for i, s := range ss {
154+
// CLI syntax is still experimental (YAML syntax is out of experimental)
155+
switch {
156+
case s == "vzNAT":
157+
expr += `{"vzNAT": true}`
158+
case strings.HasPrefix(s, "lima:"):
159+
network := strings.TrimPrefix(s, "lima:")
160+
expr += fmt.Sprintf(`{"lima": %q}`, network)
161+
default:
162+
return "", fmt.Errorf("network name must be \"vzNAT\" or \"lima:*\", got %q", s)
163+
}
164+
if i < len(ss)-1 {
165+
expr += ","
166+
}
167+
}
168+
expr += `] | .networks |= unique_by(.lima)`
169+
return expr, nil
170+
},
171+
true},
172+
{"rosetta",
173+
func(_ *flag.Flag) (string, error) {
174+
b, err := flags.GetBool("rosetta")
175+
if err != nil {
176+
return "", err
177+
}
178+
return fmt.Sprintf(".rosetta.enabled = %v | .rosetta.binfmt = %v", b, b), nil
179+
},
180+
true},
181+
{"set", d("%s"), true},
182+
{"arch", d(".arch = %q"), false},
183+
{"containerd",
184+
func(_ *flag.Flag) (string, error) {
185+
s, err := flags.GetString("containerd")
186+
if err != nil {
187+
return "", err
188+
}
189+
switch s {
190+
case "user":
191+
return `.containerd.user = true | .containerd.system = false`, nil
192+
case "system":
193+
return `.containerd.user = false | .containerd.system = true`, nil
194+
case "user+system", "system+user":
195+
return `.containerd.user = true | .containerd.system = true`, nil
196+
case "none":
197+
return `.containerd.user = false | .containerd.system = false`, nil
198+
default:
199+
return "", fmt.Errorf(`expected one of ["user", "system", "user+system", "none"], got %q`, s)
200+
}
201+
},
202+
false},
203+
204+
{"disk", d(".disk= \"%sGiB\""), false},
205+
{"vm-type", d(".vmType = %q"), false},
206+
}
207+
var exprs []string
208+
for _, def := range defs {
209+
v := flags.Lookup(def.flagName)
210+
if v != nil && v.Changed {
211+
if def.experimental {
212+
logrus.Warnf("`--%s` is experimental", def.flagName)
213+
}
214+
expr, err := def.exprFunc(v)
215+
if err != nil {
216+
return exprs, fmt.Errorf("error while processing flag %q: %w", def.flagName, err)
217+
}
218+
exprs = append(exprs, expr)
219+
}
220+
}
221+
return exprs, nil
222+
}
223+
224+
func isPowerOfTwo(x int) bool {
225+
return bits.OnesCount(uint(x)) == 1
226+
}
227+
228+
func completeCPUs(hostCPUs int) []int {
229+
var res []int
230+
for i := 1; i <= hostCPUs; i *= 2 {
231+
res = append(res, i)
232+
}
233+
if !isPowerOfTwo(hostCPUs) {
234+
res = append(res, hostCPUs)
235+
}
236+
return res
237+
}
238+
239+
func completeMemoryGiB(hostMemory uint64) []float32 {
240+
hostMemoryHalfGiB := int(hostMemory / 2 / 1024 / 1024 / 1024)
241+
var res []float32
242+
if hostMemoryHalfGiB < 1 {
243+
res = append(res, 0.5)
244+
}
245+
for i := 1; i <= hostMemoryHalfGiB; i *= 2 {
246+
res = append(res, float32(i))
247+
}
248+
if hostMemoryHalfGiB > 1 && !isPowerOfTwo(hostMemoryHalfGiB) {
249+
res = append(res, float32(hostMemoryHalfGiB))
250+
}
251+
return res
252+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package editflags
2+
3+
import (
4+
"testing"
5+
6+
"gotest.tools/v3/assert"
7+
)
8+
9+
func TestCompleteCPUs(t *testing.T) {
10+
assert.DeepEqual(t, []int{1}, completeCPUs(1))
11+
assert.DeepEqual(t, []int{1, 2}, completeCPUs(2))
12+
assert.DeepEqual(t, []int{1, 2, 4, 8}, completeCPUs(8))
13+
assert.DeepEqual(t, []int{1, 2, 4, 8, 16, 20}, completeCPUs(20))
14+
}
15+
16+
func TestCompleteMemoryGiB(t *testing.T) {
17+
assert.DeepEqual(t, []float32{0.5}, completeMemoryGiB(1<<30))
18+
assert.DeepEqual(t, []float32{1}, completeMemoryGiB(2<<30))
19+
assert.DeepEqual(t, []float32{1, 2}, completeMemoryGiB(4<<30))
20+
assert.DeepEqual(t, []float32{1, 2, 4}, completeMemoryGiB(8<<30))
21+
assert.DeepEqual(t, []float32{1, 2, 4, 8, 10}, completeMemoryGiB(20<<30))
22+
}

cmd/limactl/start.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/AlecAivazis/survey/v2"
1313
"github.com/containerd/containerd/identifiers"
14+
"github.com/lima-vm/lima/cmd/limactl/editflags"
1415
"github.com/lima-vm/lima/cmd/limactl/guessarg"
1516
"github.com/lima-vm/lima/pkg/editutil"
1617
"github.com/lima-vm/lima/pkg/ioutilx"
@@ -32,8 +33,8 @@ func registerCreateFlags(cmd *cobra.Command, commentPrefix string) {
3233
// TODO: "survey" does not support using cygwin terminal on windows yet
3334
flags.Bool("tty", isatty.IsTerminal(os.Stdout.Fd()), commentPrefix+"enable TUI interactions such as opening an editor, defaults to true when stdout is a terminal")
3435
flags.String("name", "", commentPrefix+"override the instance name")
35-
flags.String("set", "", commentPrefix+"modify the template inplace, using yq syntax")
3636
flags.Bool("list-templates", false, commentPrefix+"list available templates and exit")
37+
editflags.RegisterCreate(cmd, commentPrefix)
3738
}
3839

3940
func newCreateCommand() *cobra.Command {
@@ -47,6 +48,9 @@ To create an instance "default" from a template "docker":
4748
$ limactl create --name=default template://docker
4849
4950
To create an instance "default" with modified parameters:
51+
$ limactl create --cpus=2 --memory=2
52+
53+
To create an instance "default" with yq expressions:
5054
$ limactl create --set='.cpus = 2 | .memory = "2GiB"'
5155
5256
To see the template list:
@@ -104,20 +108,26 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
104108
err error
105109
)
106110

111+
flags := cmd.Flags()
112+
107113
// Create an instance, with menu TUI when TTY is available
108-
tty, err := cmd.Flags().GetBool("tty")
114+
tty, err := flags.GetBool("tty")
109115
if err != nil {
110116
return nil, err
111117
}
112118

113-
st.instName, err = cmd.Flags().GetString("name")
119+
st.instName, err = flags.GetString("name")
114120
if err != nil {
115121
return nil, err
116122
}
117-
st.yq, err = cmd.Flags().GetString("set")
123+
124+
yqExprs, err := editflags.YQExpressions(flags)
118125
if err != nil {
119126
return nil, err
120127
}
128+
if len(yqExprs) > 0 {
129+
st.yq = strings.Join(yqExprs, " | ")
130+
}
121131
const yBytesLimit = 4 * 1024 * 1024 // 4MiB
122132

123133
if ok, u := guessarg.SeemsTemplateURL(arg); ok {
@@ -310,7 +320,6 @@ func modifyInPlace(st *creatorState) error {
310320
if st.yq == "" {
311321
return nil
312322
}
313-
logrus.Warn("`--set` is experimental")
314323
out, err := yqutil.EvaluateExpression(st.yq, st.yBytes)
315324
if err != nil {
316325
return err

docs/experimental.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ The following features are experimental and subject to change:
1313

1414
The following commands are experimental and subject to change:
1515

16-
- `limactl (start|edit) --set=<YQ EXPRESSION>`
16+
- `limactl (create|start|edit) --set=<YQ EXPRESSION>`
17+
- `limactl (create|start|edit) --network=<NETWORK>`
1718
- `limactl snapshot *`

0 commit comments

Comments
 (0)