Repository: lima-vm/lima

Issue:
Changing the disk size in lima.yaml should resize the disk
Just running `limactl edit` does not change the actual disk image, I guess one has to run `qemu-img` manually (for now)

```
qemu-img resize ~/.lima/almalinux-8/diffdisk 30G

qemu-img info ~/.lima/almalinux-8/diffdisk
```

After that, it seems to have done the rest of the work on next start (changed it in the lima.yaml, to `disk: "30GiB"`).

_Originally posted by @afbjorklund in https://github.com/lima-vm/lima/discussions/1446#discussioncomment-5487504_

Of course it should only allow increasing the disk size, as reducing it will corrupt the existing partition.

Hints from issue comments:
@afbjorklund: When doing the `qemu-img resize`, it does require a special option before attempting to shrink an image.

           `When shrinking images, the "--shrink" option must be given. This informs qemu-img that the user acknowledges all loss of data beyond the truncated image's end.`

Most of the work for these commands to work, has to be done inside the guest image (partioning/resize2fs)

Currently there might be some scripts in place, that will resize an ext4 partition to use the new space available.

Solution patch:
diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go
index c536f1c1..0b8d8ebd 100644
--- a/cmd/limactl/edit.go
+++ b/cmd/limactl/edit.go
@@ -6,8 +6,10 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/AlecAivazis/survey/v2"
+	"github.com/lima-vm/lima/cmd/limactl/editflags"
 	"github.com/lima-vm/lima/pkg/editutil"
 	"github.com/lima-vm/lima/pkg/limayaml"
 	networks "github.com/lima-vm/lima/pkg/networks/reconcile"
@@ -30,7 +32,7 @@ func newEditCommand() *cobra.Command {
 	}
 	// TODO: "survey" does not support using cygwin terminal on windows yet
 	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")
-	editCommand.Flags().String("set", "", "modify the template inplace, using yq syntax")
+	editflags.RegisterEdit(editCommand)
 	return editCommand
 }
 
@@ -57,17 +59,18 @@ func editAction(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	tty, err := cmd.Flags().GetBool("tty")
+	flags := cmd.Flags()
+	tty, err := flags.GetBool("tty")
 	if err != nil {
 		return err
 	}
-	yq, err := cmd.Flags().GetString("set")
+	yqExprs, err := editflags.YQExpressions(flags)
 	if err != nil {
 		return err
 	}
 	var yBytes []byte
-	if yq != "" {
-		logrus.Warn("`--set` is experimental")
+	if len(yqExprs) > 0 {
+		yq := strings.Join(yqExprs, " | ")
 		yBytes, err = yqutil.EvaluateExpression(yq, yContent)
 		if err != nil {
 			return err
diff --git a/cmd/limactl/editflags/editflags.go b/cmd/limactl/editflags/editflags.go
new file mode 100644
index 00000000..52a8962f
--- /dev/null
+++ b/cmd/limactl/editflags/editflags.go
@@ -0,0 +1,252 @@
+package editflags
+
+import (
+	"fmt"
+	"math/bits"
+	"runtime"
+	"strconv"
+	"strings"
+
+	"github.com/pbnjay/memory"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+	flag "github.com/spf13/pflag"
+)
+
+// RegisterEdit registers flags related to in-place YAML modification, for `limactl edit`.
+func RegisterEdit(cmd *cobra.Command) {
+	registerEdit(cmd, "")
+}
+
+func registerEdit(cmd *cobra.Command, commentPrefix string) {
+	flags := cmd.Flags()
+
+	flags.Int("cpus", 0, commentPrefix+"number of CPUs") // Similar to colima's --cpu, but the flag name is slightly different (cpu vs cpus)
+	_ = cmd.RegisterFlagCompletionFunc("cpus", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		var res []string
+		for _, f := range completeCPUs(runtime.NumCPU()) {
+			res = append(res, strconv.Itoa(f))
+		}
+		return res, cobra.ShellCompDirectiveNoFileComp
+	})
+
+	flags.IPSlice("dns", nil, commentPrefix+"specify custom DNS (disable host resolver)") // colima-compatible
+
+	flags.Float32("memory", 0, commentPrefix+"memory in GiB") // colima-compatible
+	_ = cmd.RegisterFlagCompletionFunc("memory", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		var res []string
+		for _, f := range completeMemoryGiB(memory.TotalMemory()) {
+			res = append(res, fmt.Sprintf("%.1f", f))
+		}
+		return res, cobra.ShellCompDirectiveNoFileComp
+	})
+
+	flags.StringSlice("mount", nil, commentPrefix+"directories to mount, suffix ':w' for writable (Do not specify directories that overlap with the existing mounts)") // colima-compatible
+
+	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
+	_ = cmd.RegisterFlagCompletionFunc("mount-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		return []string{"reverse-sshfs", "9p", "virtiofs"}, cobra.ShellCompDirectiveNoFileComp
+	})
+
+	flags.Bool("mount-writable", false, commentPrefix+"make all mounts writable")
+
+	flags.StringSlice("network", nil, commentPrefix+"additional networks, e.g., \"vzNAT\" or \"lima:shared\" to assign vmnet IP")
+	_ = cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		// TODO: retrieve the lima:* network list from networks.yaml
+		return []string{"lima:shared", "lima:bridged", "lima:host", "lima:user-v2", "vzNAT"}, cobra.ShellCompDirectiveNoFileComp
+	})
+
+	flags.Bool("rosetta", false, commentPrefix+"enable Rosetta (for vz instances)")
+
+	flags.String("set", "", commentPrefix+"modify the template inplace, using yq syntax")
+}
+
+// RegisterCreate registers flags related to in-place YAML modification, for `limactl create`.
+func RegisterCreate(cmd *cobra.Command, commentPrefix string) {
+	registerEdit(cmd, commentPrefix)
+	flags := cmd.Flags()
+
+	flags.String("arch", "", commentPrefix+"machine architecture (x86_64, aarch64, riscv64)") // colima-compatible
+	_ = cmd.RegisterFlagCompletionFunc("arch", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		return []string{"x86_64", "aarch64", "riscv64"}, cobra.ShellCompDirectiveNoFileComp
+	})
+
+	flags.String("containerd", "", commentPrefix+"containerd mode (user, system, user+system, none)")
+	_ = cmd.RegisterFlagCompletionFunc("vm-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		return []string{"user", "system", "user+system", "none"}, cobra.ShellCompDirectiveNoFileComp
+	})
+
+	flags.Float32("disk", 0, commentPrefix+"disk size in GiB") // colima-compatible
+	_ = cmd.RegisterFlagCompletionFunc("memory", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		return []string{"10", "30", "50", "100", "200"}, cobra.ShellCompDirectiveNoFileComp
+	})
+
+	flags.String("vm-type", "", commentPrefix+"virtual machine type (qemu, vz)") // colima-compatible
+	_ = cmd.RegisterFlagCompletionFunc("vm-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		return []string{"qemu", "vz"}, cobra.ShellCompDirectiveNoFileComp
+	})
+}
+
+func defaultExprFunc(expr string) func(v *flag.Flag) (string, error) {
+	return func(v *flag.Flag) (string, error) {
+		return fmt.Sprintf(expr, v.Value), nil
+	}
+}
+
+// YQExpressions returns YQ expressions.
+func YQExpressions(flags *flag.FlagSet) ([]string, error) {
+	type def struct {
+		flagName     string
+		exprFunc     func(*flag.Flag) (string, error)
+		experimental bool
+	}
+	d := defaultExprFunc
+	defs := []def{
+		{"cpus", d(".cpus = %s"), false},
+		{"dns",
+			func(_ *flag.Flag) (string, error) {
+				ipSlice, err := flags.GetIPSlice("dns")
+				if err != nil {
+					return "", err
+				}
+				expr := `.dns += [`
+				for i, ip := range ipSlice {
+					expr += fmt.Sprintf("%q", ip)
+					if i < len(ipSlice)-1 {
+						expr += ","
+					}
+				}
+				expr += `] | .dns |= unique | .hostResolver.enabled=false`
+				logrus.Warnf("Disabling HostResolver, as custom DNS addresses (%v) are specified", ipSlice)
+				return expr, nil
+			},
+			false},
+		{"memory", d(".memory = \"%sGiB\""), false},
+		{"mount",
+			func(_ *flag.Flag) (string, error) {
+				ss, err := flags.GetStringSlice("mount")
+				if err != nil {
+					return "", err
+				}
+				expr := `.mounts += [`
+				for i, s := range ss {
+					writable := strings.HasSuffix(s, ":w")
+					loc := strings.TrimSuffix(s, ":w")
+					expr += fmt.Sprintf(`{"location": %q, "writable": %v}`, loc, writable)
+					if i < len(ss)-1 {
+						expr += ","
+					}
+				}
+				expr += `] | .mounts |= unique_by(.location)`
+				return expr, nil
+			},
+			false},
+		{"mount-type", d(".mountType = %q"), false},
+		{"mount-writable", d(".mounts[].writable = %s"), false},
+		{"network",
+			func(_ *flag.Flag) (string, error) {
+				ss, err := flags.GetStringSlice("network")
+				if err != nil {
+					return "", err
+				}
+				expr := `.networks += [`
+				for i, s := range ss {
+					// CLI syntax is still experimental (YAML syntax is out of experimental)
+					switch {
+					case s == "vzNAT":
+						expr += `{"vzNAT": true}`
+					case strings.HasPrefix(s, "lima:"):
+						network := strings.TrimPrefix(s, "lima:")
+						expr += fmt.Sprintf(`{"lima": %q}`, network)
+					default:
+						return "", fmt.Errorf("network name must be \"vzNAT\" or \"lima:*\", got %q", s)
+					}
+					if i < len(ss)-1 {
+						expr += ","
+					}
+				}
+				expr += `] | .networks |= unique_by(.lima)`
+				return expr, nil
+			},
+			true},
+		{"rosetta",
+			func(_ *flag.Flag) (string, error) {
+				b, err := flags.GetBool("rosetta")
+				if err != nil {
+					return "", err
+				}
+				return fmt.Sprintf(".rosetta.enabled = %v | .rosetta.binfmt = %v", b, b), nil
+			},
+			true},
+		{"set", d("%s"), true},
+		{"arch", d(".arch = %q"), false},
+		{"containerd",
+			func(_ *flag.Flag) (string, error) {
+				s, err := flags.GetString("containerd")
+				if err != nil {
+					return "", err
+				}
+				switch s {
+				case "user":
+					return `.containerd.user = true | .containerd.system = false`, nil
+				case "system":
+					return `.containerd.user = false | .containerd.system = true`, nil
+				case "user+system", "system+user":
+					return `.containerd.user = true | .containerd.system = true`, nil
+				case "none":
+					return `.containerd.user = false | .containerd.system = false`, nil
+				default:
+					return "", fmt.Errorf(`expected one of ["user", "system", "user+system", "none"], got %q`, s)
+				}
+			},
+			false},
+
+		{"disk", d(".disk= \"%sGiB\""), false},
+		{"vm-type", d(".vmType = %q"), false},
+	}
+	var exprs []string
+	for _, def := range defs {
+		v := flags.Lookup(def.flagName)
+		if v != nil && v.Changed {
+			if def.experimental {
+				logrus.Warnf("`--%s` is experimental", def.flagName)
+			}
+			expr, err := def.exprFunc(v)
+			if err != nil {
+				return exprs, fmt.Errorf("error while processing flag %q: %w", def.flagName, err)
+			}
+			exprs = append(exprs, expr)
+		}
+	}
+	return exprs, nil
+}
+
+func isPowerOfTwo(x int) bool {
+	return bits.OnesCount(uint(x)) == 1
+}
+
+func completeCPUs(hostCPUs int) []int {
+	var res []int
+	for i := 1; i <= hostCPUs; i *= 2 {
+		res = append(res, i)
+	}
+	if !isPowerOfTwo(hostCPUs) {
+		res = append(res, hostCPUs)
+	}
+	return res
+}
+
+func completeMemoryGiB(hostMemory uint64) []float32 {
+	hostMemoryHalfGiB := int(hostMemory / 2 / 1024 / 1024 / 1024)
+	var res []float32
+	if hostMemoryHalfGiB < 1 {
+		res = append(res, 0.5)
+	}
+	for i := 1; i <= hostMemoryHalfGiB; i *= 2 {
+		res = append(res, float32(i))
+	}
+	if hostMemoryHalfGiB > 1 && !isPowerOfTwo(hostMemoryHalfGiB) {
+		res = append(res, float32(hostMemoryHalfGiB))
+	}
+	return res
+}
diff --git a/cmd/limactl/start.go b/cmd/limactl/start.go
index 51e13f60..14a2a75f 100644
--- a/cmd/limactl/start.go
+++ b/cmd/limactl/start.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/AlecAivazis/survey/v2"
 	"github.com/containerd/containerd/identifiers"
+	"github.com/lima-vm/lima/cmd/limactl/editflags"
 	"github.com/lima-vm/lima/cmd/limactl/guessarg"
 	"github.com/lima-vm/lima/pkg/editutil"
 	"github.com/lima-vm/lima/pkg/ioutilx"
@@ -32,8 +33,8 @@ func registerCreateFlags(cmd *cobra.Command, commentPrefix string) {
 	// TODO: "survey" does not support using cygwin terminal on windows yet
 	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")
 	flags.String("name", "", commentPrefix+"override the instance name")
-	flags.String("set", "", commentPrefix+"modify the template inplace, using yq syntax")
 	flags.Bool("list-templates", false, commentPrefix+"list available templates and exit")
+	editflags.RegisterCreate(cmd, commentPrefix)
 }
 
 func newCreateCommand() *cobra.Command {
@@ -47,6 +48,9 @@ To create an instance "default" from a template "docker":
 $ limactl create --name=default template://docker
 
 To create an instance "default" with modified parameters:
+$ limactl create --cpus=2 --memory=2
+
+To create an instance "default" with yq expressions:
 $ limactl create --set='.cpus = 2 | .memory = "2GiB"'
 
 To see the template list:
@@ -104,20 +108,26 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
 		err error
 	)
 
+	flags := cmd.Flags()
+
 	// Create an instance, with menu TUI when TTY is available
-	tty, err := cmd.Flags().GetBool("tty")
+	tty, err := flags.GetBool("tty")
 	if err != nil {
 		return nil, err
 	}
 
-	st.instName, err = cmd.Flags().GetString("name")
+	st.instName, err = flags.GetString("name")
 	if err != nil {
 		return nil, err
 	}
-	st.yq, err = cmd.Flags().GetString("set")
+
+	yqExprs, err := editflags.YQExpressions(flags)
 	if err != nil {
 		return nil, err
 	}
+	if len(yqExprs) > 0 {
+		st.yq = strings.Join(yqExprs, " | ")
+	}
 	const yBytesLimit = 4 * 1024 * 1024 // 4MiB
 
 	if ok, u := guessarg.SeemsTemplateURL(arg); ok {
@@ -310,7 +320,6 @@ func modifyInPlace(st *creatorState) error {
 	if st.yq == "" {
 		return nil
 	}
-	logrus.Warn("`--set` is experimental")
 	out, err := yqutil.EvaluateExpression(st.yq, st.yBytes)
 	if err != nil {
 		return err
diff --git a/docs/experimental.md b/docs/experimental.md
index dd550b14..837cc750 100644
--- a/docs/experimental.md
+++ b/docs/experimental.md
@@ -13,5 +13,6 @@ The following features are experimental and subject to change:
 
 The following commands are experimental and subject to change:
 
-- `limactl (start|edit) --set=<YQ EXPRESSION>`
+- `limactl (create|start|edit) --set=<YQ EXPRESSION>`
+- `limactl (create|start|edit) --network=<NETWORK>`
 - `limactl snapshot *`
diff --git a/hack/test-templates.sh b/hack/test-templates.sh
index 190a749d..6cf56e05 100755
--- a/hack/test-templates.sh
+++ b/hack/test-templates.sh
@@ -17,8 +17,7 @@ INFO "Validating \"$FILE\""
 limactl validate "$FILE"
 
 # --cpus=1 is needed for running vz on GHA: https://github.com/lima-vm/lima/pull/1511#issuecomment-1574937888
-LIMACTL_CREATE_SET='.cpus = 1 | .memory="1GiB"'
-LIMACTL_CREATE=(limactl create --tty=false --set="${LIMACTL_CREATE_SET}")
+LIMACTL_CREATE=(limactl create --tty=false --cpus=1 --memory=1)
 
 declare -A CHECKS=(
 	["systemd"]="1"
diff --git a/pkg/yqutil/yqutil.go b/pkg/yqutil/yqutil.go
index 5184b4c6..9780e129 100644
--- a/pkg/yqutil/yqutil.go
+++ b/pkg/yqutil/yqutil.go
@@ -12,6 +12,7 @@ import (
 
 // EvaluateExpression evaluates the yq expression, and returns the modified yaml.
 func EvaluateExpression(expression string, content []byte) ([]byte, error) {
+	logrus.Debugf("Evaluating yq expression: %q", expression)
 	tmpYAMLFile, err := os.CreateTemp("", "lima-yq-*.yaml")
 	if err != nil {
 		return nil, err



Test patch:
diff --git a/cmd/limactl/editflags/editflags_test.go b/cmd/limactl/editflags/editflags_test.go
new file mode 100644
index 00000000..51d9a076
--- /dev/null
+++ b/cmd/limactl/editflags/editflags_test.go
@@ -0,0 +1,22 @@
+package editflags
+
+import (
+	"testing"
+
+	"gotest.tools/v3/assert"
+)
+
+func TestCompleteCPUs(t *testing.T) {
+	assert.DeepEqual(t, []int{1}, completeCPUs(1))
+	assert.DeepEqual(t, []int{1, 2}, completeCPUs(2))
+	assert.DeepEqual(t, []int{1, 2, 4, 8}, completeCPUs(8))
+	assert.DeepEqual(t, []int{1, 2, 4, 8, 16, 20}, completeCPUs(20))
+}
+
+func TestCompleteMemoryGiB(t *testing.T) {
+	assert.DeepEqual(t, []float32{0.5}, completeMemoryGiB(1<<30))
+	assert.DeepEqual(t, []float32{1}, completeMemoryGiB(2<<30))
+	assert.DeepEqual(t, []float32{1, 2}, completeMemoryGiB(4<<30))
+	assert.DeepEqual(t, []float32{1, 2, 4}, completeMemoryGiB(8<<30))
+	assert.DeepEqual(t, []float32{1, 2, 4, 8, 10}, completeMemoryGiB(20<<30))
+}