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