@@ -31,7 +31,7 @@ import (
31
31
func newStartCommand () * cobra.Command {
32
32
var startCommand = & cobra.Command {
33
33
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" ,
35
35
Args : cobra .MaximumNArgs (1 ),
36
36
ValidArgsFunction : startBashComplete ,
37
37
RunE : startAction ,
@@ -57,77 +57,105 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string) (*store.Instance, e
57
57
arg = args [0 ]
58
58
}
59
59
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
+ )
67
64
const yBytesLimit = 4 * 1024 * 1024 // 4MiB
68
65
69
66
if argSeemsHTTPURL (arg ) {
70
- instName , err = instNameFromURL (arg )
67
+ st . instName , err = instNameFromURL (arg )
71
68
if err != nil {
72
69
return nil , err
73
70
}
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 )
75
72
resp , err := http .Get (arg )
76
73
if err != nil {
77
74
return nil , err
78
75
}
79
76
defer resp .Body .Close ()
80
- yBytes , err = readAtMaximum (resp .Body , yBytesLimit )
77
+ st . yBytes , err = readAtMaximum (resp .Body , yBytesLimit )
81
78
if err != nil {
82
79
return nil , err
83
80
}
84
81
} else if argSeemsFileURL (arg ) {
85
- instName , err = instNameFromURL (arg )
82
+ st . instName , err = instNameFromURL (arg )
86
83
if err != nil {
87
84
return nil , err
88
85
}
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 )
90
87
r , err := os .Open (strings .TrimPrefix (arg , "file://" ))
91
88
if err != nil {
92
89
return nil , err
93
90
}
94
91
defer r .Close ()
95
- yBytes , err = readAtMaximum (r , yBytesLimit )
92
+ st . yBytes , err = readAtMaximum (r , yBytesLimit )
96
93
if err != nil {
97
94
return nil , err
98
95
}
99
96
} else if argSeemsYAMLPath (arg ) {
100
- instName , err = instNameFromYAMLPath (arg )
97
+ st . instName , err = instNameFromYAMLPath (arg )
101
98
if err != nil {
102
99
return nil , err
103
100
}
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 )
105
102
r , err := os .Open (arg )
106
103
if err != nil {
107
104
return nil , err
108
105
}
109
106
defer r .Close ()
110
- yBytes , err = readAtMaximum (r , yBytesLimit )
107
+ st . yBytes , err = readAtMaximum (r , yBytesLimit )
111
108
if err != nil {
112
109
return nil , err
113
110
}
114
111
} 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 )
119
116
}
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 )
122
119
return inst , nil
123
120
} else {
124
121
if ! errors .Is (err , os .ErrNotExist ) {
125
122
return nil , err
126
123
}
124
+ // Read the default template for creating a new instance
125
+ st .yBytes , err = readDefaultTemplate ()
126
+ if err != nil {
127
+ return nil , err
128
+ }
127
129
}
128
130
}
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 )
131
159
if err != nil {
132
160
return nil , err
133
161
}
@@ -136,92 +164,140 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string) (*store.Instance, e
136
164
maxSockName := filepath .Join (instDir , filenames .LongestSock )
137
165
if len (maxSockName ) >= osutil .UnixPathMax {
138
166
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 ))
140
168
}
141
169
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 )
143
171
}
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 )
146
175
if err != nil {
147
176
return nil , err
148
177
}
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 {
152
215
logrus .WithError (err ).Warn ("Failed to open TUI" )
153
- answerOpenEditor = false
216
+ return st , nil
154
217
}
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 {
158
224
hdr += "# - In most cases, you do not need to modify this file.\n "
159
225
}
160
226
hdr += "# - To cancel starting Lima, just save this file as an empty file.\n "
161
227
hdr += "\n "
162
228
hdr += generateEditorWarningHeader ()
163
- yBytes , err = openEditor (cmd , instName , yBytes , hdr )
229
+ var err error
230
+ st .yBytes , err = openEditor (st .instName , st .yBytes , hdr )
164
231
if err != nil {
165
- return nil , err
232
+ return st , err
166
233
}
167
- if len (yBytes ) == 0 {
234
+ if len (st . yBytes ) == 0 {
168
235
logrus .Info ("Aborting, as requested by saving the file with empty content" )
169
236
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 )),
171
249
}
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 )
172
274
}
173
- } else {
174
- logrus .Info ("Terminal is not available, proceeding without opening an editor" )
175
275
}
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 ()
179
280
if err != nil {
180
281
return nil , err
181
282
}
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 {
196
287
return nil , err
197
288
}
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
+ })
224
299
}
300
+ return res , nil
225
301
}
226
302
227
303
func fileWarning (filename string ) string {
@@ -261,7 +337,7 @@ func generateEditorWarningHeader() string {
261
337
// openEditor opens an editor, and returns the content (not path) of the modified yaml.
262
338
//
263
339
// 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 ) {
265
341
editor := editorcmd .Detect ()
266
342
if editor == "" {
267
343
return nil , errors .New ("could not detect a text editor binary, try setting $EDITOR" )
0 commit comments