Skip to content

Commit

Permalink
limactl: add --cpus, --memory, --mount-type, --vm-type, ...
Browse files Browse the repository at this point in the history
Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
  • Loading branch information
AkihiroSuda committed Jun 6, 2023
1 parent 8498a19 commit 9868bfc
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 12 deletions.
13 changes: 8 additions & 5 deletions cmd/limactl/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}

Expand All @@ -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
Expand Down
206 changes: 206 additions & 0 deletions cmd/limactl/editflags/editflags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package editflags

import (
"fmt"
"strings"

"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
)

// RegisterEdit registers flags related to in-place YAML modification.
func RegisterEdit(cmd *cobra.Command) {
flags := cmd.Flags()

flags.Int("cpus", 0, "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) {
return []string{"1", "2", "4", "8"}, cobra.ShellCompDirectiveNoFileComp
})

flags.IPSlice("dns", nil, "specify custom DNS (disable host resolver)") // colima-compatible

flags.Float32("memory", 0, "memory in GiB") // colima-compatible
_ = cmd.RegisterFlagCompletionFunc("memory", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"1", "2", "4", "8", "16", "32"}, cobra.ShellCompDirectiveNoFileComp
})

flags.StringSlice("mount", nil, "directories to mount, suffix ':w' for writable (Do not specify directories that overlap with the existing mounts)") // colima-compatible

flags.String("mount-type", "", "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, "make all mounts writable")

flags.StringSlice("network", nil, "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, "enable Rosetta (For vz instances)")

flags.String("set", "", "modify the template inplace, using yq syntax")
}

// RegisterStart registers flags related to in-place YAML modification.
func RegisterStart(cmd *cobra.Command) {
RegisterEdit(cmd)
flags := cmd.Flags()

flags.String("arch", "", "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", "", "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, "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", "", "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
}
19 changes: 14 additions & 5 deletions cmd/limactl/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -38,6 +39,9 @@ To create an instance "default" from a template "docker":
$ limactl start --name=default template://docker
To create an instance "default" with modified parameters:
$ limactl start --cpus=2 --memory=2
To create an instance "default" with yq expressions:
$ limactl start --set='.cpus = 2 | .memory = "2GiB"'
To see the template list:
Expand All @@ -59,8 +63,8 @@ $ cat template.yaml | limactl start --name=local -
}
// TODO: "survey" does not support using cygwin terminal on windows yet
startCommand.Flags().Bool("tty", isatty.IsTerminal(os.Stdout.Fd()), "enable TUI interactions such as opening an editor, defaults to true when stdout is a terminal")
editflags.RegisterStart(startCommand)
startCommand.Flags().String("name", "", "override the instance name")
startCommand.Flags().String("set", "", "modify the template inplace, using yq syntax")
startCommand.Flags().Bool("list-templates", false, "list available templates and exit")
startCommand.Flags().Duration("timeout", start.DefaultWatchHostAgentEventsTimeout, "duration to wait for the instance to be running before timing out")
return startCommand
Expand All @@ -77,20 +81,26 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string) (*store.Instance, e
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 {
Expand Down Expand Up @@ -280,7 +290,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
Expand Down
1 change: 1 addition & 0 deletions docs/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ 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 (start|edit) --network=<NETWORK>`
- `limactl snapshot *`
3 changes: 1 addition & 2 deletions hack/test-example.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ 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"'
# TODO: add "limactl create" command
LIMACTL_CREATE_AND_START=(limactl start --tty=false --set="${LIMACTL_CREATE_SET}")
LIMACTL_CREATE_AND_START=(limactl start --tty=false --cpus=1 --memory=1)

declare -A CHECKS=(
["systemd"]="1"
Expand Down
1 change: 1 addition & 0 deletions pkg/yqutil/yqutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 9868bfc

Please # to comment.