Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/cmd/limactl/edit.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
"bytes"
8
"errors"
9
"fmt"
10
"os"
11
"path/filepath"
12
13
"github.com/sirupsen/logrus"
14
"github.com/spf13/cobra"
15
16
"github.com/lima-vm/lima/v2/cmd/limactl/editflags"
17
"github.com/lima-vm/lima/v2/pkg/driverutil"
18
"github.com/lima-vm/lima/v2/pkg/editutil"
19
"github.com/lima-vm/lima/v2/pkg/instance"
20
"github.com/lima-vm/lima/v2/pkg/limatype"
21
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
22
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
23
"github.com/lima-vm/lima/v2/pkg/limayaml"
24
networks "github.com/lima-vm/lima/v2/pkg/networks/reconcile"
25
"github.com/lima-vm/lima/v2/pkg/store"
26
"github.com/lima-vm/lima/v2/pkg/uiutil"
27
"github.com/lima-vm/lima/v2/pkg/yqutil"
28
)
29
30
func newEditCommand() *cobra.Command {
31
editCommand := &cobra.Command{
32
Use: "edit INSTANCE|FILE.yaml",
33
Short: "Edit an instance of Lima or a template",
34
Args: WrapArgsError(cobra.MaximumNArgs(1)),
35
RunE: editAction,
36
ValidArgsFunction: editBashComplete,
37
GroupID: basicCommand,
38
}
39
editflags.RegisterEdit(editCommand, "")
40
return editCommand
41
}
42
43
func editAction(cmd *cobra.Command, args []string) error {
44
ctx := cmd.Context()
45
var arg string
46
if len(args) > 0 {
47
arg = args[0]
48
}
49
50
var filePath string
51
var err error
52
var inst *limatype.Instance
53
54
if arg == "" {
55
arg = DefaultInstanceName
56
}
57
if err := dirnames.ValidateInstName(arg); err == nil {
58
inst, err = store.Inspect(ctx, arg)
59
if err != nil {
60
if errors.Is(err, os.ErrNotExist) {
61
return fmt.Errorf("instance %q not found", arg)
62
}
63
return err
64
}
65
if inst.Status == limatype.StatusRunning {
66
return errors.New("cannot edit a running instance")
67
}
68
filePath = filepath.Join(inst.Dir, filenames.LimaYAML)
69
} else {
70
// absolute path is required for `limayaml.Validate`
71
filePath, err = filepath.Abs(arg)
72
if err != nil {
73
return err
74
}
75
}
76
77
yContent, err := os.ReadFile(filePath)
78
if err != nil {
79
return err
80
}
81
flags := cmd.Flags()
82
tty, err := flags.GetBool("tty")
83
if err != nil {
84
return err
85
}
86
yqExprs, err := editflags.YQExpressions(flags, false)
87
if err != nil {
88
return err
89
}
90
var yBytes []byte
91
if len(yqExprs) > 0 {
92
yq := yqutil.Join(yqExprs)
93
yBytes, err = yqutil.EvaluateExpression(yq, yContent)
94
if err != nil {
95
return err
96
}
97
} else if tty {
98
var hdr string
99
if inst != nil {
100
hdr = fmt.Sprintf("# Please edit the following configuration for Lima instance %q\n", inst.Name)
101
} else {
102
hdr = fmt.Sprintf("# Please edit the following configuration %q\n", filePath)
103
}
104
hdr += "# and an empty file will abort the edit.\n"
105
hdr += "\n"
106
hdr += editutil.GenerateEditorWarningHeader()
107
yBytes, err = editutil.OpenEditor(ctx, yContent, hdr)
108
if err != nil {
109
return err
110
}
111
}
112
if len(yBytes) == 0 {
113
logrus.Info("Aborting, as requested by saving the file with empty content")
114
return nil
115
}
116
if bytes.Equal(yBytes, yContent) {
117
logrus.Info("Aborting, no changes made to the instance")
118
return nil
119
}
120
y, err := limayaml.LoadWithWarnings(ctx, yBytes, filePath)
121
if err != nil {
122
return err
123
}
124
if err := driverutil.ResolveVMType(ctx, y, filePath); err != nil {
125
return fmt.Errorf("failed to resolve vm for %q: %w", filePath, err)
126
}
127
if err := limayaml.Validate(y, true); err != nil {
128
return saveRejectedYAML(yBytes, err)
129
}
130
131
if err := limayaml.ValidateAgainstLatestConfig(ctx, yBytes, yContent); err != nil {
132
return saveRejectedYAML(yBytes, err)
133
}
134
135
if err := os.WriteFile(filePath, yBytes, 0o644); err != nil {
136
return err
137
}
138
139
if inst != nil {
140
logrus.Infof("Instance %q configuration edited", inst.Name)
141
}
142
143
if !tty {
144
// use "start" to start it
145
return nil
146
}
147
if inst == nil {
148
// edited a limayaml file directly
149
return nil
150
}
151
startNow, err := askWhetherToStart()
152
if err != nil {
153
return err
154
}
155
if !startNow {
156
return nil
157
}
158
err = networks.Reconcile(ctx, inst.Name)
159
if err != nil {
160
return err
161
}
162
163
// store.Inspect() syncs values between inst.YAML and the store.
164
// This call applies the validated template to the store.
165
inst, err = store.Inspect(ctx, inst.Name)
166
if err != nil {
167
return err
168
}
169
return instance.Start(ctx, inst, "", false, false)
170
}
171
172
func askWhetherToStart() (bool, error) {
173
message := "Do you want to start the instance now? "
174
return uiutil.Confirm(message, true)
175
}
176
177
func editBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
178
return bashCompleteInstanceNames(cmd)
179
}
180
181
// saveRejectedYAML writes the rejected config and returns an error.
182
func saveRejectedYAML(y []byte, origErr error) error {
183
rejectedYAML := "lima.REJECTED.yaml"
184
if writeErr := os.WriteFile(rejectedYAML, y, 0o644); writeErr != nil {
185
return fmt.Errorf("the YAML is invalid, attempted to save the buffer as %q but failed: %w", rejectedYAML, errors.Join(writeErr, origErr))
186
}
187
// TODO: may need to support editing the rejected YAML
188
return fmt.Errorf("the YAML is invalid, saved the buffer as %q: %w", rejectedYAML, origErr)
189
}
190
191