Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
252 changes: 252 additions & 0 deletions cmd/limactl/editflags/editflags.go
Original file line number Diff line number Diff line change
@@ -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
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also add "disk" here, ("disk in GiB")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some concern here, because disk size can only ever be increased. The only way to shrink disk size is by destroying and re-creating the instance, losing all your data.

So this is not something that you should casually do. Or it should require a confirmation prompt for the user to acknowledge (disabled by --tty=false).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For new instances it can be casually done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For new instances it can be casually done

Yes, my concern is purely about limectl edit default --disk 500GiB, and then being unable to ever shrink it again.

Copy link
Member

@afbjorklund afbjorklund May 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could disable (or prompt) the feature to edit the disk size, until grow and shrink has been implemented ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, --disk should not be implemented until Lima can grow the disk automatically.

FWIW, I don't expect us to ever be able to shrink disks; is this even possible?


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
}
22 changes: 22 additions & 0 deletions cmd/limactl/editflags/editflags_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
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 All @@ -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 {
Expand All @@ -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:
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion docs/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 *`
Loading