Skip to content

Commit 05e1e3f

Browse files
committed
limactl start: show menu for choosing examples (docker, archlinux, ...)
Signed-off-by: Akihiro Suda <[email protected]>
1 parent 186a840 commit 05e1e3f

File tree

3 files changed

+173
-87
lines changed

3 files changed

+173
-87
lines changed

cmd/limactl/edit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func editAction(cmd *cobra.Command, args []string) error {
5656
hdr += "# and an empty file will abort the edit.\n"
5757
hdr += "\n"
5858
hdr += generateEditorWarningHeader()
59-
yBytes, err := openEditor(cmd, instName, yContent, hdr)
59+
yBytes, err := openEditor(instName, yContent, hdr)
6060
if err != nil {
6161
return err
6262
}

cmd/limactl/info.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ func newInfoCommand() *cobra.Command {
2020
return infoCommand
2121
}
2222

23+
type TemplateYAML struct {
24+
Name string `json:"name"`
25+
Location string `json:"location"`
26+
}
27+
2328
type Info struct {
2429
Version string `json:"version"`
30+
Templates []TemplateYAML `json:"templates"`
2531
DefaultTemplate *limayaml.LimaYAML `json:"defaultTemplate"`
2632
LimaHome string `json:"limaHome"`
2733
// TODO: add diagnostic info of QEMU
@@ -40,6 +46,10 @@ func infoAction(cmd *cobra.Command, args []string) error {
4046
Version: version.Version,
4147
DefaultTemplate: y,
4248
}
49+
info.Templates, err = listTemplateYAMLs()
50+
if err != nil {
51+
return err
52+
}
4353
info.LimaHome, err = dirnames.LimaDir()
4454
if err != nil {
4555
return err

cmd/limactl/start.go

Lines changed: 162 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import (
3131
func newStartCommand() *cobra.Command {
3232
var startCommand = &cobra.Command{
3333
Use: "start NAME|FILE.yaml|URL",
34-
Short: fmt.Sprintf("Start an instance of Lima. If the instance does not exist, open an editor for creating new one, with name %q", DefaultInstanceName),
34+
Short: "Start an instance of Lima",
3535
Args: cobra.MaximumNArgs(1),
3636
ValidArgsFunction: startBashComplete,
3737
RunE: startAction,
@@ -57,77 +57,105 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string) (*store.Instance, e
5757
arg = args[0]
5858
}
5959

60-
yBytes, err := readDefaultTemplate()
61-
if err != nil {
62-
return nil, err
63-
}
64-
65-
var instName string
66-
60+
var (
61+
st = &creatorState{}
62+
err error
63+
)
6764
const yBytesLimit = 4 * 1024 * 1024 // 4MiB
6865

6966
if argSeemsHTTPURL(arg) {
70-
instName, err = instNameFromURL(arg)
67+
st.instName, err = instNameFromURL(arg)
7168
if err != nil {
7269
return nil, err
7370
}
74-
logrus.Debugf("interpreting argument %q as a http url for instance %q", arg, instName)
71+
logrus.Debugf("interpreting argument %q as a http url for instance %q", arg, st.instName)
7572
resp, err := http.Get(arg)
7673
if err != nil {
7774
return nil, err
7875
}
7976
defer resp.Body.Close()
80-
yBytes, err = readAtMaximum(resp.Body, yBytesLimit)
77+
st.yBytes, err = readAtMaximum(resp.Body, yBytesLimit)
8178
if err != nil {
8279
return nil, err
8380
}
8481
} else if argSeemsFileURL(arg) {
85-
instName, err = instNameFromURL(arg)
82+
st.instName, err = instNameFromURL(arg)
8683
if err != nil {
8784
return nil, err
8885
}
89-
logrus.Debugf("interpreting argument %q as a file url for instance %q", arg, instName)
86+
logrus.Debugf("interpreting argument %q as a file url for instance %q", arg, st.instName)
9087
r, err := os.Open(strings.TrimPrefix(arg, "file://"))
9188
if err != nil {
9289
return nil, err
9390
}
9491
defer r.Close()
95-
yBytes, err = readAtMaximum(r, yBytesLimit)
92+
st.yBytes, err = readAtMaximum(r, yBytesLimit)
9693
if err != nil {
9794
return nil, err
9895
}
9996
} else if argSeemsYAMLPath(arg) {
100-
instName, err = instNameFromYAMLPath(arg)
97+
st.instName, err = instNameFromYAMLPath(arg)
10198
if err != nil {
10299
return nil, err
103100
}
104-
logrus.Debugf("interpreting argument %q as a file path for instance %q", arg, instName)
101+
logrus.Debugf("interpreting argument %q as a file path for instance %q", arg, st.instName)
105102
r, err := os.Open(arg)
106103
if err != nil {
107104
return nil, err
108105
}
109106
defer r.Close()
110-
yBytes, err = readAtMaximum(r, yBytesLimit)
107+
st.yBytes, err = readAtMaximum(r, yBytesLimit)
111108
if err != nil {
112109
return nil, err
113110
}
114111
} else {
115-
instName = arg
116-
logrus.Debugf("interpreting argument %q as an instance name %q", arg, instName)
117-
if err := identifiers.Validate(instName); err != nil {
118-
return nil, fmt.Errorf("argument must be either an instance name or a YAML file path, got %q: %w", instName, err)
112+
st.instName = arg
113+
logrus.Debugf("interpreting argument %q as an instance name %q", arg, st.instName)
114+
if err := identifiers.Validate(st.instName); err != nil {
115+
return nil, fmt.Errorf("argument must be either an instance name or a YAML file path, got %q: %w", st.instName, err)
119116
}
120-
if inst, err := store.Inspect(instName); err == nil {
121-
logrus.Infof("Using the existing instance %q", instName)
117+
if inst, err := store.Inspect(st.instName); err == nil {
118+
logrus.Infof("Using the existing instance %q", st.instName)
122119
return inst, nil
123120
} else {
124121
if !errors.Is(err, os.ErrNotExist) {
125122
return nil, err
126123
}
124+
// Read the default template for creating a new instance
125+
st.yBytes, err = readDefaultTemplate()
126+
if err != nil {
127+
return nil, err
128+
}
127129
}
128130
}
129-
// create a new instance from the template
130-
instDir, err := store.InstanceDir(instName)
131+
132+
// Create an instance, with menu TUI when TTY is available
133+
tty, err := cmd.Flags().GetBool("tty")
134+
if err != nil {
135+
return nil, err
136+
}
137+
if tty {
138+
var err error
139+
st, err = chooseNextCreatorState(st)
140+
if err != nil {
141+
return nil, err
142+
}
143+
} else {
144+
logrus.Info("Terminal is not available, proceeding without opening an editor")
145+
}
146+
saveBrokenEditorBuffer := tty
147+
return createInstance(st, saveBrokenEditorBuffer)
148+
}
149+
150+
func createInstance(st *creatorState, saveBrokenEditorBuffer bool) (*store.Instance, error) {
151+
if st.instName == "" {
152+
return nil, errors.New("got empty st.instName")
153+
}
154+
if len(st.yBytes) == 0 {
155+
return nil, errors.New("got empty st.yBytes")
156+
}
157+
158+
instDir, err := store.InstanceDir(st.instName)
131159
if err != nil {
132160
return nil, err
133161
}
@@ -136,92 +164,140 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string) (*store.Instance, e
136164
maxSockName := filepath.Join(instDir, filenames.LongestSock)
137165
if len(maxSockName) >= osutil.UnixPathMax {
138166
return nil, fmt.Errorf("instance name %q too long: %q must be less than UNIX_PATH_MAX=%d characters, but is %d",
139-
instName, maxSockName, osutil.UnixPathMax, len(maxSockName))
167+
st.instName, maxSockName, osutil.UnixPathMax, len(maxSockName))
140168
}
141169
if _, err := os.Stat(instDir); !errors.Is(err, os.ErrNotExist) {
142-
return nil, fmt.Errorf("instance %q already exists (%q)", instName, instDir)
170+
return nil, fmt.Errorf("instance %q already exists (%q)", st.instName, instDir)
143171
}
144-
145-
tty, err := cmd.Flags().GetBool("tty")
172+
// limayaml.Load() needs to pass the store file path to limayaml.FillDefault() to calculate default MAC addresses
173+
filePath := filepath.Join(instDir, filenames.LimaYAML)
174+
y, err := limayaml.Load(st.yBytes, filePath)
146175
if err != nil {
147176
return nil, err
148177
}
149-
if tty {
150-
answerOpenEditor, err := askWhetherToOpenEditor(instName)
151-
if err != nil {
178+
if err := limayaml.Validate(*y, true); err != nil {
179+
if !saveBrokenEditorBuffer {
180+
return nil, err
181+
}
182+
rejectedYAML := "lima.REJECTED.yaml"
183+
if writeErr := os.WriteFile(rejectedYAML, st.yBytes, 0644); writeErr != nil {
184+
return nil, fmt.Errorf("the YAML is invalid, attempted to save the buffer as %q but failed: %v: %w", rejectedYAML, writeErr, err)
185+
}
186+
return nil, fmt.Errorf("the YAML is invalid, saved the buffer as %q: %w", rejectedYAML, err)
187+
}
188+
if err := os.MkdirAll(instDir, 0700); err != nil {
189+
return nil, err
190+
}
191+
if err := os.WriteFile(filePath, st.yBytes, 0644); err != nil {
192+
return nil, err
193+
}
194+
return store.Inspect(st.instName)
195+
}
196+
197+
type creatorState struct {
198+
instName string // instance name
199+
yBytes []byte // yaml bytes
200+
}
201+
202+
func chooseNextCreatorState(st *creatorState) (*creatorState, error) {
203+
for {
204+
var ans string
205+
prompt := &survey.Select{
206+
Message: fmt.Sprintf("Creating an instance %q", st.instName),
207+
Options: []string{
208+
"Proceed with the current configuration",
209+
"Open an editor to review or modify the current configuration",
210+
"Choose another example (docker, podman, archlinux, fedora, ...)",
211+
"Exit",
212+
},
213+
}
214+
if err := survey.AskOne(prompt, &ans); err != nil {
152215
logrus.WithError(err).Warn("Failed to open TUI")
153-
answerOpenEditor = false
216+
return st, nil
154217
}
155-
if answerOpenEditor {
156-
hdr := fmt.Sprintf("# Review and modify the following configuration for Lima instance %q.\n", instName)
157-
if instName == DefaultInstanceName {
218+
switch ans {
219+
case prompt.Options[0]: // "Proceed with the current configuration"
220+
return st, nil
221+
case prompt.Options[1]: // "Open an editor ..."
222+
hdr := fmt.Sprintf("# Review and modify the following configuration for Lima instance %q.\n", st.instName)
223+
if st.instName == DefaultInstanceName {
158224
hdr += "# - In most cases, you do not need to modify this file.\n"
159225
}
160226
hdr += "# - To cancel starting Lima, just save this file as an empty file.\n"
161227
hdr += "\n"
162228
hdr += generateEditorWarningHeader()
163-
yBytes, err = openEditor(cmd, instName, yBytes, hdr)
229+
var err error
230+
st.yBytes, err = openEditor(st.instName, st.yBytes, hdr)
164231
if err != nil {
165-
return nil, err
232+
return st, err
166233
}
167-
if len(yBytes) == 0 {
234+
if len(st.yBytes) == 0 {
168235
logrus.Info("Aborting, as requested by saving the file with empty content")
169236
os.Exit(0)
170-
return nil, errors.New("should not reach here")
237+
return st, errors.New("should not reach here")
238+
}
239+
return st, nil
240+
case prompt.Options[2]: // "Choose another example..."
241+
examples, err := listTemplateYAMLs()
242+
if err != nil {
243+
return st, err
244+
}
245+
var ansEx int
246+
promptEx := &survey.Select{
247+
Message: "Choose an example",
248+
Options: make([]string, len(examples)),
171249
}
250+
for i := range examples {
251+
promptEx.Options[i] = examples[i].Name
252+
}
253+
if err := survey.AskOne(promptEx, &ansEx); err != nil {
254+
return st, err
255+
}
256+
if ansEx > len(examples)-1 {
257+
return st, fmt.Errorf("invalid answer %d for %d entries", ansEx, len(examples))
258+
}
259+
yamlPath := examples[ansEx].Location
260+
st.instName, err = instNameFromYAMLPath(yamlPath)
261+
if err != nil {
262+
return nil, err
263+
}
264+
st.yBytes, err = os.ReadFile(yamlPath)
265+
if err != nil {
266+
return nil, err
267+
}
268+
continue
269+
case prompt.Options[3]: // "Exit"
270+
os.Exit(0)
271+
return st, errors.New("should not reach here")
272+
default:
273+
return st, fmt.Errorf("unexpected answer %q", ans)
172274
}
173-
} else {
174-
logrus.Info("Terminal is not available, proceeding without opening an editor")
175275
}
176-
// limayaml.Load() needs to pass the store file path to limayaml.FillDefault() to calculate default MAC addresses
177-
filePath := filepath.Join(instDir, filenames.LimaYAML)
178-
y, err := limayaml.Load(yBytes, filePath)
276+
}
277+
278+
func listTemplateYAMLs() ([]TemplateYAML, error) {
279+
usrlocalsharelimaDir, err := usrlocalsharelima.Dir()
179280
if err != nil {
180281
return nil, err
181282
}
182-
if err := limayaml.Validate(*y, true); err != nil {
183-
if !tty {
184-
return nil, err
185-
}
186-
rejectedYAML := "lima.REJECTED.yaml"
187-
if writeErr := os.WriteFile(rejectedYAML, yBytes, 0644); writeErr != nil {
188-
return nil, fmt.Errorf("the YAML is invalid, attempted to save the buffer as %q but failed: %v: %w", rejectedYAML, writeErr, err)
189-
}
190-
return nil, fmt.Errorf("the YAML is invalid, saved the buffer as %q: %w", rejectedYAML, err)
191-
}
192-
if err := os.MkdirAll(instDir, 0700); err != nil {
193-
return nil, err
194-
}
195-
if err := os.WriteFile(filePath, yBytes, 0644); err != nil {
283+
examplesDir := filepath.Join(usrlocalsharelimaDir, "examples")
284+
glob := filepath.Join(examplesDir, "*.yaml")
285+
globbed, err := filepath.Glob(glob)
286+
if err != nil {
196287
return nil, err
197288
}
198-
return store.Inspect(instName)
199-
}
200-
201-
func askWhetherToOpenEditor(name string) (bool, error) {
202-
var ans string
203-
prompt := &survey.Select{
204-
Message: fmt.Sprintf("Creating an instance %q", name),
205-
Options: []string{
206-
"Proceed with the default configuration",
207-
"Open an editor to override the configuration",
208-
"Exit",
209-
},
210-
}
211-
if err := survey.AskOne(prompt, &ans); err != nil {
212-
return false, err
213-
}
214-
switch ans {
215-
case prompt.Options[0]:
216-
return false, nil
217-
case prompt.Options[1]:
218-
return true, nil
219-
case prompt.Options[2]:
220-
os.Exit(0)
221-
return false, errors.New("should not reach here")
222-
default:
223-
return false, fmt.Errorf("unexpected answer %q", ans)
289+
var res []TemplateYAML
290+
for _, f := range globbed {
291+
base := filepath.Base(f)
292+
if strings.HasPrefix(base, ".") {
293+
continue
294+
}
295+
res = append(res, TemplateYAML{
296+
Name: strings.TrimSuffix(filepath.Base(f), ".yaml"),
297+
Location: f,
298+
})
224299
}
300+
return res, nil
225301
}
226302

227303
func fileWarning(filename string) string {
@@ -261,7 +337,7 @@ func generateEditorWarningHeader() string {
261337
// openEditor opens an editor, and returns the content (not path) of the modified yaml.
262338
//
263339
// openEditor returns nil when the file was saved as an empty file, optionally with whitespaces.
264-
func openEditor(cmd *cobra.Command, name string, content []byte, hdr string) ([]byte, error) {
340+
func openEditor(name string, content []byte, hdr string) ([]byte, error) {
265341
editor := editorcmd.Detect()
266342
if editor == "" {
267343
return nil, errors.New("could not detect a text editor binary, try setting $EDITOR")

0 commit comments

Comments
 (0)