Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/cmd/limactl/start.go
1645 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package main
5
6
import (
7
"context"
8
"errors"
9
"fmt"
10
"os"
11
"path/filepath"
12
"runtime"
13
"strings"
14
15
"github.com/sirupsen/logrus"
16
"github.com/spf13/cobra"
17
"github.com/spf13/pflag"
18
19
"github.com/lima-vm/lima/v2/cmd/limactl/editflags"
20
"github.com/lima-vm/lima/v2/pkg/driverutil"
21
"github.com/lima-vm/lima/v2/pkg/editutil"
22
"github.com/lima-vm/lima/v2/pkg/instance"
23
"github.com/lima-vm/lima/v2/pkg/limatmpl"
24
"github.com/lima-vm/lima/v2/pkg/limatype"
25
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
26
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
27
"github.com/lima-vm/lima/v2/pkg/limayaml"
28
networks "github.com/lima-vm/lima/v2/pkg/networks/reconcile"
29
"github.com/lima-vm/lima/v2/pkg/registry"
30
"github.com/lima-vm/lima/v2/pkg/store"
31
"github.com/lima-vm/lima/v2/pkg/templatestore"
32
"github.com/lima-vm/lima/v2/pkg/uiutil"
33
"github.com/lima-vm/lima/v2/pkg/yqutil"
34
)
35
36
func registerCreateFlags(cmd *cobra.Command, commentPrefix string) {
37
flags := cmd.Flags()
38
flags.String("name", "", commentPrefix+"Override the instance name")
39
flags.Bool("list-templates", false, commentPrefix+"List available templates and exit")
40
flags.Bool("list-drivers", false, commentPrefix+"List available drivers and exit")
41
editflags.RegisterCreate(cmd, commentPrefix)
42
}
43
44
func newCreateCommand() *cobra.Command {
45
createCommand := &cobra.Command{
46
Use: "create FILE.yaml|URL",
47
Example: `
48
To create an instance "default" from the default Ubuntu template:
49
$ limactl create
50
51
To create an instance "default" from a template "docker":
52
$ limactl create --name=default template://docker
53
54
To create an instance "default" with modified parameters:
55
$ limactl create --cpus=2 --memory=2
56
57
To create an instance "default" with yq expressions:
58
$ limactl create --set='.cpus = 2 | .memory = "2GiB"'
59
60
To see the template list:
61
$ limactl create --list-templates
62
63
To create an instance "default" from a local file:
64
$ limactl create --name=default /usr/local/share/lima/templates/fedora.yaml
65
66
To create an instance "default" from a remote URL (use carefully, with a trustable source):
67
$ limactl create --name=default https://raw.githubusercontent.com/lima-vm/lima/master/templates/alpine.yaml
68
69
To create an instance "local" from a template passed to stdin (--name parameter is required):
70
$ cat template.yaml | limactl create --name=local -
71
`,
72
Short: "Create an instance of Lima",
73
Args: WrapArgsError(cobra.MaximumNArgs(1)),
74
ValidArgsFunction: createBashComplete,
75
RunE: createAction,
76
GroupID: basicCommand,
77
}
78
registerCreateFlags(createCommand, "")
79
return createCommand
80
}
81
82
func newStartCommand() *cobra.Command {
83
startCommand := &cobra.Command{
84
Use: "start NAME|FILE.yaml|URL",
85
Example: `
86
To create an instance "default" (if not created yet) from the default Ubuntu template, and start it:
87
$ limactl start
88
89
To create an instance "default" from a template "docker", and start it:
90
$ limactl start --name=default template://docker
91
`,
92
Short: "Start an instance of Lima",
93
Args: WrapArgsError(cobra.MaximumNArgs(1)),
94
ValidArgsFunction: startBashComplete,
95
RunE: startAction,
96
GroupID: basicCommand,
97
}
98
registerCreateFlags(startCommand, "[limactl create] ")
99
if runtime.GOOS != "windows" {
100
startCommand.Flags().Bool("foreground", false, "Run the hostagent in the foreground")
101
}
102
startCommand.Flags().Duration("timeout", instance.DefaultWatchHostAgentEventsTimeout, "Duration to wait for the instance to be running before timing out")
103
startCommand.Flags().Bool("progress", false, "Show provision script progress by tailing cloud-init logs")
104
startCommand.SetHelpFunc(func(cmd *cobra.Command, _ []string) {
105
printCommandSummary(cmd)
106
107
allFlags, createFlags := collectFlags(cmd)
108
printFlags(allFlags, createFlags)
109
110
printGlobalFlags(cmd)
111
})
112
113
return startCommand
114
}
115
116
func printCommandSummary(cmd *cobra.Command) {
117
fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n", cmd.Short)
118
fmt.Fprintf(cmd.OutOrStdout(), "Usage:\n %s\n\n", cmd.UseLine())
119
120
if cmd.Example != "" {
121
fmt.Fprintf(cmd.OutOrStdout(), "Examples:\n%s\n\n", cmd.Example)
122
}
123
}
124
125
func getFlagType(flag *pflag.Flag) string {
126
switch flag.Value.Type() {
127
case "bool":
128
return ""
129
case "string":
130
return "string"
131
case "int":
132
return "int"
133
case "duration":
134
return "duration"
135
case "stringSlice", "stringArray":
136
return "strings"
137
case "ipSlice":
138
return "ipSlice"
139
case "uint16":
140
return "uint16"
141
case "float32":
142
return "float32"
143
default:
144
return flag.Value.Type()
145
}
146
}
147
148
func formatFlag(flag *pflag.Flag) (flagName, shorthand string) {
149
flagName = "--" + flag.Name
150
151
if flag.Shorthand != "" {
152
shorthand = "-" + flag.Shorthand
153
}
154
155
flagType := getFlagType(flag)
156
if flagType != "" {
157
flagName += " " + flagType
158
}
159
160
return flagName, shorthand
161
}
162
163
func collectFlags(cmd *cobra.Command) (allFlags, createFlags []string) {
164
cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) {
165
flagName, shorthand := formatFlag(flag)
166
flagUsage := flag.Usage
167
168
var formattedFlag string
169
if shorthand != "" {
170
formattedFlag = fmt.Sprintf(" %s, %s", shorthand, flagName)
171
} else {
172
formattedFlag = fmt.Sprintf(" %s", flagName)
173
}
174
175
if strings.HasPrefix(flagUsage, "[limactl create]") {
176
cleanUsage := strings.TrimPrefix(flagUsage, "[limactl create] ")
177
createFlags = append(createFlags, fmt.Sprintf("%-25s %s", formattedFlag, cleanUsage))
178
} else {
179
allFlags = append(allFlags, fmt.Sprintf("%-25s %s", formattedFlag, flagUsage))
180
}
181
})
182
return allFlags, createFlags
183
}
184
185
func printFlags(allFlags, createFlags []string) {
186
if len(allFlags) > 0 {
187
fmt.Fprint(os.Stdout, "Flags:\n")
188
for _, flag := range allFlags {
189
fmt.Fprintln(os.Stdout, flag)
190
}
191
fmt.Fprint(os.Stdout, "\n")
192
}
193
194
if len(createFlags) > 0 {
195
fmt.Fprint(os.Stdout, "Flags inherited from `limactl create`:\n")
196
for _, flag := range createFlags {
197
fmt.Fprintln(os.Stdout, flag)
198
}
199
fmt.Fprint(os.Stdout, "\n")
200
}
201
}
202
203
func printGlobalFlags(cmd *cobra.Command) {
204
if cmd.HasAvailableInheritedFlags() {
205
fmt.Fprintf(cmd.OutOrStdout(), "Global Flags:\n%s", cmd.InheritedFlags().FlagUsages())
206
}
207
}
208
209
func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*limatype.Instance, error) {
210
ctx := cmd.Context()
211
var arg string // can be empty
212
if len(args) > 0 {
213
arg = args[0]
214
}
215
216
flags := cmd.Flags()
217
218
// Create an instance, with menu TUI when TTY is available
219
tty, err := flags.GetBool("tty")
220
if err != nil {
221
return nil, err
222
}
223
224
name, err := flags.GetString("name")
225
if err != nil {
226
return nil, err
227
}
228
if name != "" {
229
err := dirnames.ValidateInstName(name)
230
if err != nil {
231
return nil, err
232
}
233
}
234
if isTemplateURL, templateName := limatmpl.SeemsTemplateURL(arg); isTemplateURL {
235
switch templateName {
236
case "experimental/vz":
237
logrus.Warn("template://experimental/vz was merged into the default template in Lima v1.0. See also <https://lima-vm.io/docs/config/vmtype/>.")
238
case "experimental/riscv64":
239
logrus.Warn("template://experimental/riscv64 was merged into the default template in Lima v1.0. Use `limactl create --arch=riscv64 template://default` instead.")
240
case "experimental/armv7l":
241
logrus.Warn("template://experimental/armv7l was merged into the default template in Lima v1.0. Use `limactl create --arch=armv7l template://default` instead.")
242
case "vmnet":
243
logrus.Warn("template://vmnet was removed in Lima v1.0. Use `limactl create --network=lima:shared template://default` instead. See also <https://lima-vm.io/docs/config/network/>.")
244
case "experimental/net-user-v2":
245
logrus.Warn("template://experimental/net-user-v2 was removed in Lima v1.0. Use `limactl create --network=lima:user-v2 template://default` instead. See also <https://lima-vm.io/docs/config/network/>.")
246
case "experimental/9p":
247
logrus.Warn("template://experimental/9p was removed in Lima v1.0. Use `limactl create --vm-type=qemu --mount-type=9p template://default` instead. See also <https://lima-vm.io/docs/config/mount/>.")
248
case "experimental/virtiofs-linux":
249
logrus.Warn("template://experimental/virtiofs-linux was removed in Lima v1.0. Use `limactl create --mount-type=virtiofs template://default` instead. See also <https://lima-vm.io/docs/config/mount/>.")
250
}
251
}
252
if arg == "-" {
253
if name == "" {
254
return nil, errors.New("must pass instance name with --name when reading template from stdin")
255
}
256
// see if the tty was set explicitly or not
257
ttySet := cmd.Flags().Changed("tty")
258
if ttySet && tty {
259
return nil, errors.New("cannot use --tty=true when reading template from stdin")
260
}
261
tty = false
262
}
263
var tmpl *limatmpl.Template
264
if err := dirnames.ValidateInstName(arg); arg == "" || err == nil {
265
tmpl = &limatmpl.Template{Name: name}
266
if arg == "" {
267
if name == "" {
268
tmpl.Name = DefaultInstanceName
269
}
270
} else {
271
logrus.Debugf("interpreting argument %q as an instance name", arg)
272
if name != "" && name != arg {
273
return nil, fmt.Errorf("instance name %q and CLI flag --name=%q cannot be specified together", arg, tmpl.Name)
274
}
275
tmpl.Name = arg
276
}
277
// store.Inspect() will validate the template name (in case it has been set to arg)
278
inst, err := store.Inspect(ctx, tmpl.Name)
279
if err == nil {
280
if createOnly {
281
return nil, fmt.Errorf("instance %q already exists", tmpl.Name)
282
}
283
logrus.Infof("Using the existing instance %q", tmpl.Name)
284
yqExprs, err := editflags.YQExpressions(flags, false)
285
if err != nil {
286
return nil, err
287
}
288
if len(yqExprs) > 0 {
289
yq := yqutil.Join(yqExprs)
290
inst, err = applyYQExpressionToExistingInstance(ctx, inst, yq)
291
if err != nil {
292
return nil, fmt.Errorf("failed to apply yq expression %q to instance %q: %w", yq, tmpl.Name, err)
293
}
294
}
295
return inst, nil
296
}
297
if !errors.Is(err, os.ErrNotExist) {
298
return nil, err
299
}
300
if arg != "" && arg != DefaultInstanceName {
301
logrus.Infof("Creating an instance %q from template://default (Not from template://%s)", tmpl.Name, tmpl.Name)
302
logrus.Warnf("This form is deprecated. Use `limactl create --name=%s template://default` instead", tmpl.Name)
303
}
304
// Read the default template for creating a new instance
305
tmpl.Bytes, err = templatestore.Read(templatestore.Default)
306
if err != nil {
307
return nil, err
308
}
309
} else {
310
tmpl, err = limatmpl.Read(cmd.Context(), name, arg)
311
if err != nil {
312
return nil, err
313
}
314
if createOnly {
315
// store.Inspect() will also validate the instance name
316
if _, err := store.Inspect(ctx, tmpl.Name); err == nil {
317
return nil, fmt.Errorf("instance %q already exists", tmpl.Name)
318
}
319
} else if err := dirnames.ValidateInstName(tmpl.Name); err != nil {
320
return nil, err
321
}
322
}
323
324
if err := tmpl.Embed(cmd.Context(), true, true); err != nil {
325
return nil, err
326
}
327
yqExprs, err := editflags.YQExpressions(flags, true)
328
if err != nil {
329
return nil, err
330
}
331
yq := yqutil.Join(yqExprs)
332
if tty {
333
var err error
334
tmpl, err = chooseNextCreatorState(cmd.Context(), tmpl, yq)
335
if err != nil {
336
return nil, err
337
}
338
} else {
339
logrus.Info("Terminal is not available, proceeding without opening an editor")
340
if err := modifyInPlace(tmpl, yq); err != nil {
341
return nil, err
342
}
343
}
344
saveBrokenYAML := tty
345
return instance.Create(cmd.Context(), tmpl.Name, tmpl.Bytes, saveBrokenYAML)
346
}
347
348
func applyYQExpressionToExistingInstance(ctx context.Context, inst *limatype.Instance, yq string) (*limatype.Instance, error) {
349
if strings.TrimSpace(yq) == "" {
350
return inst, nil
351
}
352
filePath := filepath.Join(inst.Dir, filenames.LimaYAML)
353
yContent, err := os.ReadFile(filePath)
354
if err != nil {
355
return nil, err
356
}
357
logrus.Debugf("Applying yq expression %q to an existing instance %q", yq, inst.Name)
358
yBytes, err := yqutil.EvaluateExpression(yq, yContent)
359
if err != nil {
360
return nil, err
361
}
362
y, err := limayaml.Load(ctx, yBytes, filePath)
363
if err != nil {
364
return nil, err
365
}
366
if err := driverutil.ResolveVMType(ctx, y, filePath); err != nil {
367
return nil, fmt.Errorf("failed to resolve vm for %q: %w", filePath, err)
368
}
369
if err := limayaml.Validate(y, true); err != nil {
370
rejectedYAML := "lima.REJECTED.yaml"
371
if writeErr := os.WriteFile(rejectedYAML, yBytes, 0o644); writeErr != nil {
372
return nil, fmt.Errorf("the YAML is invalid, attempted to save the buffer as %q but failed: %w: %w", rejectedYAML, writeErr, err)
373
}
374
// TODO: may need to support editing the rejected YAML
375
return nil, fmt.Errorf("the YAML is invalid, saved the buffer as %q: %w", rejectedYAML, err)
376
}
377
if err := os.WriteFile(filePath, yBytes, 0o644); err != nil {
378
return nil, err
379
}
380
// Reload
381
return store.Inspect(ctx, inst.Name)
382
}
383
384
func modifyInPlace(st *limatmpl.Template, yq string) error {
385
out, err := yqutil.EvaluateExpression(yq, st.Bytes)
386
if err != nil {
387
return err
388
}
389
st.Bytes = out
390
return nil
391
}
392
393
// exitSuccessError is an error that indicates a successful exit.
394
type exitSuccessError struct {
395
Msg string
396
}
397
398
// Error implements error.
399
func (e exitSuccessError) Error() string {
400
return e.Msg
401
}
402
403
// ExitCode implements ExitCoder.
404
func (exitSuccessError) ExitCode() int {
405
return 0
406
}
407
408
func chooseNextCreatorState(ctx context.Context, tmpl *limatmpl.Template, yq string) (*limatmpl.Template, error) {
409
for {
410
if err := modifyInPlace(tmpl, yq); err != nil {
411
logrus.WithError(err).Warn("Failed to evaluate yq expression")
412
return tmpl, err
413
}
414
message := fmt.Sprintf("Creating an instance %q", tmpl.Name)
415
options := []string{
416
"Proceed with the current configuration",
417
"Open an editor to review or modify the current configuration",
418
"Choose another template (docker, podman, archlinux, fedora, ...)",
419
"Exit",
420
}
421
ans, err := uiutil.Select(message, options)
422
if err != nil {
423
if errors.Is(err, uiutil.InterruptErr) {
424
logrus.Fatal("Interrupted by user")
425
}
426
logrus.WithError(err).Warn("Failed to open TUI")
427
return tmpl, nil
428
}
429
switch ans {
430
case 0: // "Proceed with the current configuration"
431
return tmpl, nil
432
case 1: // "Open an editor ..."
433
hdr := fmt.Sprintf("# Review and modify the following configuration for Lima instance %q.\n", tmpl.Name)
434
if tmpl.Name == DefaultInstanceName {
435
hdr += "# - In most cases, you do not need to modify this file.\n"
436
}
437
hdr += "# - To cancel starting Lima, just save this file as an empty file.\n"
438
hdr += "\n"
439
hdr += editutil.GenerateEditorWarningHeader()
440
var err error
441
tmpl.Bytes, err = editutil.OpenEditor(ctx, tmpl.Bytes, hdr)
442
tmpl.Config = nil
443
if err != nil {
444
return tmpl, err
445
}
446
if len(tmpl.Bytes) == 0 {
447
const msg = "Aborting, as requested by saving the file with empty content"
448
logrus.Info(msg)
449
return nil, exitSuccessError{Msg: msg}
450
}
451
err = tmpl.Embed(ctx, true, true)
452
if err != nil {
453
return nil, err
454
}
455
return tmpl, nil
456
case 2: // "Choose another template..."
457
templates, err := filterHiddenTemplates()
458
if err != nil {
459
return tmpl, err
460
}
461
message := "Choose a template"
462
options := make([]string, len(templates))
463
for i := range templates {
464
options[i] = templates[i].Name
465
}
466
ansEx, err := uiutil.Select(message, options)
467
if err != nil {
468
return tmpl, err
469
}
470
if ansEx > len(templates)-1 {
471
return tmpl, fmt.Errorf("invalid answer %d for %d entries", ansEx, len(templates))
472
}
473
yamlPath := templates[ansEx].Location
474
if tmpl.Name == "" {
475
tmpl.Name, err = limatmpl.InstNameFromYAMLPath(yamlPath)
476
if err != nil {
477
return nil, err
478
}
479
}
480
tmpl, err = limatmpl.Read(ctx, tmpl.Name, yamlPath)
481
if err != nil {
482
return nil, err
483
}
484
err = tmpl.Embed(ctx, true, true)
485
if err != nil {
486
return nil, err
487
}
488
continue
489
case 3: // "Exit"
490
return nil, exitSuccessError{Msg: "Choosing to exit"}
491
default:
492
return tmpl, fmt.Errorf("unexpected answer %q", ans)
493
}
494
}
495
}
496
497
// createStartActionCommon is shared by createAction and startAction.
498
func createStartActionCommon(cmd *cobra.Command, _ []string) (exit bool, err error) {
499
if listTemplates, err := cmd.Flags().GetBool("list-templates"); err != nil {
500
return true, err
501
} else if listTemplates {
502
templates, err := filterHiddenTemplates()
503
if err != nil {
504
return true, err
505
}
506
w := cmd.OutOrStdout()
507
for _, f := range templates {
508
_, _ = fmt.Fprintln(w, f.Name)
509
}
510
return true, nil
511
} else if listDrivers, err := cmd.Flags().GetBool("list-drivers"); err != nil {
512
return true, err
513
} else if listDrivers {
514
w := cmd.OutOrStdout()
515
for k := range registry.List() {
516
_, _ = fmt.Fprintln(w, k)
517
}
518
return true, nil
519
}
520
return false, nil
521
}
522
523
func filterHiddenTemplates() ([]templatestore.Template, error) {
524
templates, err := templatestore.Templates()
525
if err != nil {
526
return nil, err
527
}
528
var filtered []templatestore.Template
529
for _, f := range templates {
530
// Don't show internal base templates like `_default/*` and `_images/*`.
531
if !strings.HasPrefix(f.Name, "_") {
532
filtered = append(filtered, f)
533
}
534
}
535
return filtered, nil
536
}
537
538
func createAction(cmd *cobra.Command, args []string) error {
539
if exit, err := createStartActionCommon(cmd, args); err != nil {
540
return err
541
} else if exit {
542
return nil
543
}
544
inst, err := loadOrCreateInstance(cmd, args, true)
545
if err != nil {
546
return err
547
}
548
if len(inst.Errors) > 0 {
549
return fmt.Errorf("errors inspecting instance: %+v", inst.Errors)
550
}
551
if _, err = instance.Prepare(cmd.Context(), inst); err != nil {
552
return err
553
}
554
logrus.Infof("Run `limactl start %s` to start the instance.", inst.Name)
555
return nil
556
}
557
558
func startAction(cmd *cobra.Command, args []string) error {
559
if exit, err := createStartActionCommon(cmd, args); err != nil {
560
return err
561
} else if exit {
562
return nil
563
}
564
inst, err := loadOrCreateInstance(cmd, args, false)
565
if err != nil {
566
return err
567
}
568
if len(inst.Errors) > 0 {
569
return fmt.Errorf("errors inspecting instance: %+v", inst.Errors)
570
}
571
switch inst.Status {
572
case limatype.StatusRunning:
573
logrus.Infof("The instance %q is already running. Run `%s` to open the shell.",
574
inst.Name, instance.LimactlShellCmd(inst.Name))
575
// Not an error
576
return nil
577
case limatype.StatusStopped:
578
// NOP
579
default:
580
logrus.Warnf("expected status %q, got %q", limatype.StatusStopped, inst.Status)
581
}
582
ctx := cmd.Context()
583
err = networks.Reconcile(ctx, inst.Name)
584
if err != nil {
585
return err
586
}
587
588
launchHostAgentForeground := false
589
if runtime.GOOS != "windows" {
590
foreground, err := cmd.Flags().GetBool("foreground")
591
if err != nil {
592
return err
593
}
594
launchHostAgentForeground = foreground
595
}
596
timeout, err := cmd.Flags().GetDuration("timeout")
597
if err != nil {
598
return err
599
}
600
if timeout > 0 {
601
ctx = instance.WithWatchHostAgentTimeout(ctx, timeout)
602
}
603
604
progress, err := cmd.Flags().GetBool("progress")
605
if err != nil {
606
return err
607
}
608
609
return instance.Start(ctx, inst, "", launchHostAgentForeground, progress)
610
}
611
612
func createBashComplete(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
613
return bashCompleteTemplateNames(cmd, toComplete)
614
}
615
616
func startBashComplete(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
617
compInst, _ := bashCompleteInstanceNames(cmd)
618
compTmpl, _ := bashCompleteTemplateNames(cmd, toComplete)
619
return append(compInst, compTmpl...), cobra.ShellCompDirectiveDefault
620
}
621
622